diff --git a/Common/js/build-include.js b/Common/js/build-include.js index 9b69a0c8..b7afc5f9 100644 --- a/Common/js/build-include.js +++ b/Common/js/build-include.js @@ -1,3 +1,3 @@ window.codapPluginConfig = { - buildNumber: "0063" + buildNumber: "0064" } diff --git a/TP-Sampler/src/codap-com.js b/TP-Sampler/src/codap-com.js index 84d9b7fc..69b9bc67 100644 --- a/TP-Sampler/src/codap-com.js +++ b/TP-Sampler/src/codap-com.js @@ -274,7 +274,7 @@ CodapCom.prototype = { if (oldDeviceName !== deviceName) { codapInterface.sendRequest({ action: "update", - resource: `dataContext[${targetDataSetName}].collection[items].attribute[${oldDeviceName}]`, + resource: `dataContext[${targetDataSetName}].collection[${collectionNames.items}].attribute[${oldDeviceName}]`, values: { "name": deviceName } @@ -315,13 +315,13 @@ CodapCom.prototype = { } else { codapInterface.sendRequest({ action: "get", - resource: `dataContext[${targetDataSetName}].collection[items].attributeList`, + resource: `dataContext[${targetDataSetName}].collection[${collectionNames.items}].attributeList`, }).then((res) => { const {values} = res; if (!values.length || !values.find((attr) => attr.name === deviceName)) { codapInterface.sendRequest({ action: "create", - resource: `dataContext[${targetDataSetName}].collection[items].attribute`, + resource: `dataContext[${targetDataSetName}].collection[${collectionNames.items}].attribute`, values: [ { name: deviceName, diff --git a/bin/build b/bin/build index 7dc1a3fe..c2cb1010 100755 --- a/bin/build +++ b/bin/build @@ -60,7 +60,11 @@ function buildPlugins() { PLUGIN_BUILD_DIR=target fi echo Building $plugin - ( cd $plugin && npm install && npm run build && cp -r $PLUGIN_BUILD_DIR "$BUILD_DIR/$PLUGIN_NAME" ) + if [ "$plugin" == "../noaa-codap-plugin" ] ; then + ( cd $plugin && npm install && npm run build && cp -r "dist" "$BUILD_DIR/$PLUGIN_NAME" ) + else + ( cd $plugin && npm install && npm run build && cp -r $PLUGIN_BUILD_DIR "$BUILD_DIR/$PLUGIN_NAME" ) + fi done } diff --git a/eepsmedia/plugins/testimate/Programmer Guide.md b/eepsmedia/plugins/testimate/Programmer Guide.md new file mode 100644 index 00000000..cdb83072 --- /dev/null +++ b/eepsmedia/plugins/testimate/Programmer Guide.md @@ -0,0 +1,450 @@ +# testimate programmer's guide + +## Overall design + +The user drops attributes onto the "x" and "y" attribute positions. +The idea is that if there's only one attribute, it's `x`. +The second attribute is `y`. + +If there are two attributes, `x` is the outcome and `y` is the predictor. +This is wrong, traditionally, but it's (probably) too late to change! +For some tests, such as a two-sample *t*, both are equivalent. + +`testimate` evaluates the variables to see what kind they are. +It judges them to be _numeric_, _categorical_, or _binary_, +that is categorical with only two categories. + +Once the two variables are established, +`testimate` presents the user with all the tests appropriate to that set of variables. +These appear in a menu. +The moment that menu item is changed, `testimate` recalculates everything and displays the results +of that test and/or estimate. + +The user can press buttons to "emit" data into CODAP. +These are test parameters and results, and go into a separate collection. +If the test has been changed, the dataset is constructed anew. +(If it were not, the attributes would not be the same.) + +## The update loop +We want to show the user a valid test and its results whenever any attribute has been dropped +into a slot in the plugin. +This happens not just on the drop of an attribute, +but whenever anything happens that could conceivably affect those results; +that way the display is always current. +So if the user changes the test, we update. +If the user changes the alpha level, we update. +If a case value changes, we update. + +These actions result in calls to various handlers, +most of which are in `handlers.js`. +The handlers call "update central" in the form of +`testimate.refreshDataAndTestResults()`. + +An example is the handler that gets called when a user picks a new test from the menu: + +```javascript + changeTest: function() { + const T = document.getElementById(`testMenu`); + testimate.makeFreshTest(T.value); // the testID, need for state and restore + testimate.refreshDataAndTestResults(); + } +``` + +Other handlers include notification handlers (like for case or attribute changes in CODAP) +that are more elaborate and may reside in `connect.js` or `data.js` +depending on whether I've gotten around to refactoring... + +### testimate.refreshDataAndTestResults() + +Here is that method, with various console.logs removed: + +``` + refreshDataAndTestResults: async function () { + if (this.state.dataset) { + await data.updateData(); + await data.makeXandYArrays(data.allCODAPitems); + this.dirtyData = false; // todo: do we need this any more? + + this.checkTestConfiguration(); // ensure that this.theTest holds a suitable "Test" + + if (this.theTest && this.theTest.testID) { + this.adjustTestSides(); // todo: figure out if this is correct; shouldn't we compute the value before we do this? + data.removeInappropriateCases(); // depends on the test's parameters being known (paired, numeric, etc) + await this.theTest.updateTestResults(); // with the right data and the test, we can calculate these results. + } + } + ui.redraw(); + }, +``` + +This method is responsible for getting everything ready for display, +and then calling `ui.redraw()`, which makes the display happen. + +* `data.updateData()` gets the data from CODAP as an array of items, `data.allCODAPitems`. +* `data.makeXandYArrays()` processes those case-based objects into [one or] two Arrays, +containing the values for the [one or] two attributes. These are of class `AttData`, +and hold additional information, such as whether the attribute is numeric or categorical. +* `this.checkTestConfiguration()` creates a list of all compatible tests; +if the current test is not on the list, it picks a new one and +stores it in `testimate.theTest`. +* `this.adjustTestSides()` may be gone soon, but it takes care of the one- or two-sided nature of the test. +* `data.removeInappropriateCases()` adjusts the x and y arrays depending on the test. +For example, in a regression, it ensures that each pair of values are both numeric. +The result is cleaned arrays suitable for the stats package we use for calculation. +* `this.theTest.updateTestResults()` actually performs the calculations, +storing results in an object `theTest.results`. + + +## Tests + +Each test (and its associated estimate, if any) is represented +by a class that inherits from `Test`. +In that file (`src/Test.js`) is a static menber, `Test.configs`, +which is an object whose keys are...well, here's one: + +``` + NN02: { + id: `NN02`, + name: 'two-sample t', + xType: 'numeric', + yType: 'numeric', + paired: false, + groupAxis : "", + emitted: `N,P,mean1,mean2,diff,t,conf,CImin,CImax`, + testing: `diff`, + makeMenuString: ( ) => {return TwoSampleT.makeMenuString(`NN02`);}, + fresh: (ix) => { return new TwoSampleT(ix, false) }, + }, +``` + +The `id`, `NN02`, means that x and y are `N`umeric and that this is the second (of four) +tests that you can do if both your attributes are numeric. +This particular one is an un-paired, two-sample _t_ procedure, +testing or estimating the difference of means between two attributes. +(The other three `NN` are a one-sample _t_ — using only `x` —; +a paired two-sample _t_; +and linear regression.) + +The configuration above also gives you hints about other things. +For example, the class `TwoSampleT` is the subclass of `Test` in which the test is performed. +Its code is in `src/test/two-sample-t.js`. +We also infer that it will have a (`static`) method called `makeMenuString(iConfigID)`, +and as you might expect it's responsible for creating the text of the menu item +that the user can choose. + +Note: in this case, the _paired_ test also refers to the `TwoSampleT` class, but in its configuration, +the `paired` member is set to `true`. +That way, the test instantiation can know to make the appropriate calculations and +construct the appropriate output. + +### Test subclasses + +There are a total of 10 subclasses of `Test` as of this writing (Nov 2023). +All tests, like `TwoSampleT`, have `makeMenuString`. +Besides `makeMenuString()`, they have several methods in common as well, for example: + +#### updateTestResults() +...does the actual calculation. +It adds to an object called `results` that belongs to the instance of the test. +An example for two-sample _t_ is the calculation of standard error. +Its line is + +``` + this.results.SE = this.results.s * Math.sqrt((1 / this.results.N1) + (1 / this.results.N2)); +``` + +which is, of course,different from the corresponding line in the `OneSampleT` class: + +``` + this.results.SE = this.results.s / Math.sqrt(this.results.N); +``` +#### makeResultsString() +...creates a string (HTML) that gets put into the DOM so the user can see it. +It basically takes elements of `this.results` (such as `this.results.SE`) +and inserts them into a big string (called `out`) that the method constructs and returns. + +In that method, the results items are first converted into strings: + +```agsl + const SE = ui.numberToString(this.results.SE); +``` + +The utility `ui.numberToString()` takes an optional argument for the number of places (default: 4). +It also collapses large numbers (to make, e.g., `3.7602 M` for 3.76 million) and, if necessary, +resorts to scientific notation. +this is important because the user really doesn't want to see all the available places. + +Ultimately, these string elements get substituted into `out`; this shows the +part that outputs the confidence interval: + +``` + out += `
diff = ${diff}, ${conf}% CI = [${CImin}, ${CImax}] `; +``` + +#### makeConfigureGuts() +...makes the HTML that fills the "configuration" stripe taht appears below the results. +There, the user specifies important _parameters_ for the test +such as + +* the value you are testing against +* whether you are doing a one-or two-sided procedure +* the confidence level you want for the estimate + +These are stored not in `results` but in the global `state` variable, +such as `testimate.state.testParams.conf`, which is the confidence level. + +### Test parameters + +When you perform a test, you do so using a number of parameters such as +the alpha level and whether it is 1- or 2-sided. +These parameters vary from test to test. +They are used extensively in each test's `updatetestResults()` method. + +The current parameters are an object stored in a `state` field, +that is, `testimate.state.testParams`. +Users change test parameters using the "configuration" section +of the display, created using the `makeConfigureGuts()` +method for each type of test. + +> Note: do not confuse this user-facing "configuration" with the _test_ configuration, +> often `theConfig`, which specifies things about the _type_ of +> test, such as whether its variables must be numeric or categorical. +> Those "configs" live in the important and extensive +> static object, `Test.configs`. + +The tricky bit is that we want `testimate` to remember the parameters +for each type of test, so that if the user changes from (say) +a one-sample *p* test to a test for independence, +and then switches back, the parameters are restored. + +This is accomplished using another `state` field, +`testimate.state.testParamDictionary`, which is keyed by the +`testID`, that is, the four-character label such as `C_01`. +Each value in that dictionary is the entire `testParam` object, +kind of like this: + +```javascript +testParamDictionary : { + BB01 : { + alpha : 0.05 + conf : 95 + focusGroupX : "sick" + focusGroupY : "treatment" + reversed : false + sides : 2 + value : 0 + } +} +``` + +Note that this dictionary, keyed by test type, +is different from (and independent of) the "focus group" dictionaries, which +are keyed by attribute name. + +Tests are created anew frequently, and when they are, a new set of `testParams` +gets created as well. +The dance between saved values and defaults for a new test takes +place in the (parent class) `Test` constructor, like this: + +```javascript +if (testimate.state.testParamDictionary[iID]) { + testimate.state.testParams = testimate.state.testParamDictionary[iID]; +} else { + testimate.state.testParams = {...Test.defaultTestParams, ...this.theConfig.paramExceptions}; +} +``` +There are not many `paramExceptions`; each type of test has a set, mostly empty. +However, as an example, the one-sample _p_ test has a default `value` of 0.5, +while all other tests use 0 as a default. + +When the user uses a control to change a parameter, that happens in +a handler (in `handlers.js`). +For example, changing `value` looks like this: + +```javascript +changeValue: function() { + const v = document.getElementById(`valueBox`); + testimate.state.testParams.value = v.value; + testimate.refreshDataAndTestResults(); +} +``` +Then the handler calls `refreshDataAndTestResults()`, where, if +we find that there IS a working test, we save the current `testParams` +in the dictionary: + +```javascript + if (this.theTest && this.theTest.testID) { + // remember the test parameters for this type of test + testimate.state.testParamDictionary[testimate.theTest.testID] = testimate.state.testParams; +... etc ... +``` + + +## Communicating with CODAP + +* User drops attributes into drop-target objects in the UI. +This lets them specify what variables they are testing. +This requires working with drop notifications. +* User can ask for test/estimate results to be emitted into a +CODAP file. +* In the case of multiple, repeated sampling and testing, +this requires interacting with `rerandomize`. + +## Emitting data into CODAP + +The user can emit test results into CODAP. +This creates a new dataset with attributes that contain +test or estimate results such as `P` or `CIMax`. +The user chooses an `emitMode` (a member of `ui`, i.e., it's `ui.emitMode` in `ui.js`) +with one of three values: `single`, `random`, or `hierarchy`. + +`single` is self-explanatory: you get one case. + +With `random`, you choose a number of iterations. +The plugin re-randomizes the source collection that many times, +performing the tests and emitting the results. + +With `hierarchy`, the plugin performs the test once +for every case in the top level of the hierarchy. +This option does not appear if the dataset is flat or of there is only one case at the top. + +## Getting and using CODAP data + +CODAP data is case-based. +If you get a set of CODAP *items*, they come as an array of objects, +where each object has a `values` member, an `Object` of key-value pairs +that correspond to an attribute name and its value. + +But to use the stat library we use for computation, we need an attribute-based +data structure, that is, for each attribute we're using, an array +that contains the values. +Of course, if we're using two attributes, the cases are connected by having +the indices correspond; +that is, the two arrays have to stay in order. + +So here is the process, most of which is the responsibility of the `data` singleton, +located in `src/data.js`. + +### Get data from CODAP + +We get the whole source dataset at once in the form of items. +This get triggered in `ui.redraw()`: + +```javascript +await data.updateData(); // make sure we have the current data +``` +That method, `data.updateData()`, contains these lines: + +```javascript +this.sourceDatasetInfo = await connect.getSourceDatasetInfo(testimate.state.dataset.name); +this.hasRandom = this.sourceDSHasRandomness(); +this.isGrouped = this.sourceDSisHierarchical(); + +this.topCases = (this.isGrouped) ? await connect.retrieveTopLevelCases() : []; + +await this.retrieveAllItemsFromCODAP(); +``` +`this.sourceDatasetInfo` comes from a get-dataContext call, and contains information +on the structure of the dataset, that is, collections and attributes---but no data. +We use that to find whether the dataset is hierarchical, +and (three lines later) to get the top-level cases. + +`this.hasRandom` is `true` if any of the attributes has a formula with `"random"` in it. + +`this.isGrouped` is true if there is more than one collection. + +`this.topCases` is a case-based array of objects containing attributes and values +of the top-level collection (empty if `isGrouped` is false). + +Then, finally, we retrieve all the items, the actual data. +Here is the method, which is full of important side effects: + +```javascript + retrieveAllItemsFromCODAP: async function () { + if (testimate.state.x) { + this.dataset = await connect.getAllItems(); // this.dataset is now set as array of objects (result.values) + if (this.dataset) { + this.xAttData = new AttData(testimate.state.x, this.dataset); + this.yAttData = new AttData(testimate.state.y, this.dataset); + } + } else { + console.log(`no x variable`); + } + }, +``` + +If an `x` variable has been specified, we set `this.dataset` to that case-based array of items. +Each item has a `values` member, which is the key-value object with the data. +This has *all* of the attributes. + +Then we create these two new objects, `this.xAttData` and `this.yAttData`, +which are attribute-based arrays extracted from `data.dataset`. + +**Key takeaway**: +The method `data.retrieveAllItemsFromCODAP()` is a model for how to convert +an array of case-based objects into the attribute-based arrays +that you need to perform tests. + +### Aside: the AttData class + +The class `AttData` is defined in `data.js`, down at the bottom. +Its members are declared in the constructor like this: + +```javascript +this.name = iAtt ? iAtt.name : null; +this.theRawArray = []; +this.theArray = []; // stays empty in constructor +this.valueSet = new Set(); +this.missingCount = 0; +this.numericCount = 0; +this.nonNumericCount = 0; +this.defaultType = ""; +``` + +You can see that an `AttData` has a bunch of useful stuff in it. +The constructor goes on to process the information from `data.dataset`, +cleaning things up and making numbers out of numeric strings. +At the same time, it creates a `valueSet` that lets us see +the set of values, important for a categorical attribute +that we might use for grouping. +The constructor also counts up missing and numeric values, +and stuffs the clean values into `this.rawArray`. + +Later, we process `this.rawArray` into `this.theArray`, which is what gets used +when calculating test results. + +### Making AttData.theArray + +In the "update" method, `testimate.refreshDataAndTestResults()`, +we call `data.makeXandYArrays(data.allCODAPitems)`. +This is where `data.XattDataX` and `data.YattData` get created. +After some additional, processingwe call `data.removeInappropriateCases()`. +This method populates the `.theArray` members of the x and y `AttData`s. + +This involves several nitty-gritty steps such as, +if the test requires numeric attributes (e.g., difference of means), +it replaces any non-numeric values with `null`. + +### Performing the tests + +Every test has an `updateTestResults()` method (also called by `testimate.refreshDataAndTestResults()`). +Those methods use the `theArray` members to get the data. + +Here is a snippet from `regression.js`: + +```javascript +for (let i = 0; i < N; i++) { + // Note how these definitions are REVERSED. + // we want to look at the var in the first position (xAttData) as the dependent variable (Y) + const X = data.yAttData.theArray[i]; + const Y = data.xAttData.theArray[i]; +``` + +Notice how we use the same indices to enforce the underlying case identities. + +(Also, because this is linear regression, the attribute on the LEFT, +which we call `xAttData`, +is the one to be predicted, that is, `Y` in the regression.) + +## Localization +We use `POEditor` to help with localization. \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/README.md b/eepsmedia/plugins/testimate/README.md new file mode 100644 index 00000000..414cb0d8 --- /dev/null +++ b/eepsmedia/plugins/testimate/README.md @@ -0,0 +1,29 @@ +# testimate + +This plugin lets you perform the traditional, frequentist, parametric tests that AP Stats requires. + +Drag attributes onto the spaces provided. +Put the principal, "outcome" variable in the space on the left. +For a test with only one variable, put the corresponding attribute there. + +For tests with two variables, put the secondary (or predictor) variable on the right. + +Choose a suitable test from the menu. +The menu shows the tests that "work" given the types of attributes in the spaces. +The plugin tries to infer types from the data, +but sometimes you have to tell it, +for example, with the Titanic `Class` attribute, which has values of 1, 2, and 3, but is really categorical. +To change the type of an attribute, click the `123/abc` button. + +# Available tests + +| | numeric on left | categorical on left | +|------------------|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| nothing on the right | test mean (t) | test proportion (z)
test proportion (binomial)
goodness of fit (chisq) | +| numeric on right | difference of means (t, two sample)
paired difference (t)
linear regression
correlation | logistic regression
| +| categorical on right | difference of means (t, grouped)
one-way ANOVA (F) | difference of proportions (z)
difference of proportions(grouped)
independence (chisq) | + +## ANOVA example +This is the most complicated display, of course, but gives you the basic idea: + +![](art/testimateANOVAsample.png) diff --git a/eepsmedia/plugins/testimate/art/info.png b/eepsmedia/plugins/testimate/art/info.png new file mode 100644 index 00000000..cfbd8e0a Binary files /dev/null and b/eepsmedia/plugins/testimate/art/info.png differ diff --git a/eepsmedia/plugins/testimate/art/testimateANOVAsample.png b/eepsmedia/plugins/testimate/art/testimateANOVAsample.png new file mode 100644 index 00000000..952ddd16 Binary files /dev/null and b/eepsmedia/plugins/testimate/art/testimateANOVAsample.png differ diff --git a/eepsmedia/plugins/testimate/css/testimate.css b/eepsmedia/plugins/testimate/css/testimate.css new file mode 100644 index 00000000..99416888 --- /dev/null +++ b/eepsmedia/plugins/testimate/css/testimate.css @@ -0,0 +1,220 @@ + +/* + define variables for css! + */ +:root { + --codap-color : #72bfca; + --bg-body-color : lightgray; + --drag-near-bg-color : #cba; + --drag-over-bg-color : #fed; + --drag-none-bg-color : lightblue; + --drag-empty-bg-color : lightgray; + +} + +body { + margin : 0; + padding : 0; + + color : #333; + font-family: "Sirin Stencil",sans-serif; + font-size: small; + min-height: 100vh; /* make fill the plugin */ + background-color: var(--bg-body-color); +} + +pre { + margin : 0; + padding : 0; + font-family: "Nanum Gothic Coding", monospace; +} + +details { + padding-top: 1em; +} + +summary { + font-weight: bold; + padding-bottom: 0.5em; + cursor : pointer; +} + +#titleDIV { + width: 100%; + color: white; + padding: 0.5em; + font-weight: 700; + font-size: large; +} + +#datasetDIV { + display: flex; + align-items: center; +} + +#datasetDIV div:last-child { + margin-left: auto; +} + +#datasetSPAN { + padding : 4px; +} + +#attributeStrip { + width : 100%; + background-color: var(--bg-body-color); +} + +.attributeHint { + font-size: x-small; + font-weight: 400; +} + +#xCNbutton, #yCNbutton { + background-color: lightslategrey; + color : white; + border-radius: 6px; + margin : 0.2em; + padding: 0.2em; + cursor: pointer; +} + +#Xbackdrop, #Ybackdrop { + width : 50%; + padding : 0.5em; + background-color: aliceblue; +} + +.textButton { + cursor: pointer; +} + +.testimateButton { + background-color: lightslategrey; + color : white; + border-radius: 6px; + margin : 0.2em; + padding: 0.2em; + cursor: pointer; +} + +.chiclet { + font-size: 10px; + cursor: pointer; +} + +.infoIcon { + width: 20px; + height: 20px; + padding: 4px; +} + +#xDIV, #yDIV { + border-radius: 6px; + font-weight: 700; + font-size: large; + color: black; + padding: 0.5em; +} + +#testHeaderDIV, #configureDIV { + width: 100%; + display : block; + background-color: lightblue; + color: black; + padding: 0.5em; + font-size: 12px; + line-height: 1.2em; + +} + +#resultsDIV, #emitDIV { + width: 100%; + display : block; + background-color: lightgray; + color: black; + padding: 0.5em; + font-size: 12px; + line-height: 1.2em; +} + + +.short_number_field { + width : 4em; + font-family: "Nanum Gothic Coding", monospace; + font-size: small; +} + +.probability { + +} + +/* test results table classes */ + +.test-results { + border-spacing: 1.0em 0; + font-family: "Nanum Gothic Coding", monospace; +background-color: #eee; + padding-top : 0.5em; + padding-bottom: 0.5em; +} + +.headerRow { + border-bottom: blue; + /* + border-width: 4px; + background-color: lightblue; + */ +} + +table tr:nth-child(odd) td{ + background-color: #aaa; + padding: 0.5em; +} +table tr:nth-child(even) td{ + background-color: #ccc; + padding: 0.5em; +} + +/* drag-drop highlight classes */ + +.drag-none { + background-color: var(--drag-none-bg-color); +} +.drag-near { + background-color: var(--drag-near-bg-color); +} +.drag-over { + background-color: var(--drag-over-bg-color); +} +.drag-empty { + background-color: var(--drag-empty-bg-color); +} + +/* BOX CLASSES */ + +.hBox { + display: flex; + flex-direction: row; + /*display: -webkit-box; + display: -moz-box; + display: box; + -webkit-box-orient: horizontal; + -moz-box-orient: horizontal; + box-orient: horizontal;*/ +} + +.vBox { + display: flex; + flex-direction: column; + /* + display: -webkit-box; + display: -moz-box; + display: box; + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + box-orient: vertical; + */ +} + + diff --git a/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Bold.ttf b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Bold.ttf new file mode 100644 index 00000000..36bd2206 Binary files /dev/null and b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Bold.ttf differ diff --git a/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Regular.ttf b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Regular.ttf new file mode 100644 index 00000000..990066a7 Binary files /dev/null and b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/NanumGothicCoding-Regular.ttf differ diff --git a/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/OFL.txt b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/OFL.txt new file mode 100644 index 00000000..62104f73 --- /dev/null +++ b/eepsmedia/plugins/testimate/fonts/Nanum_Gothic_Coding/OFL.txt @@ -0,0 +1,97 @@ +Copyright (c) 2010, NHN Corporation (http://www.nhncorp.com), +with Reserved Font Name Nanum, Naver Nanum, NanumGothic, Naver +NanumGothic, NanumMyeongjo, Naver NanumMyeongjo, NanumBrush, Naver +NanumBrush, NanumPen, Naver NanumPen. + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/eepsmedia/plugins/testimate/index.html b/eepsmedia/plugins/testimate/index.html new file mode 100644 index 00000000..de5c8414 --- /dev/null +++ b/eepsmedia/plugins/testimate/index.html @@ -0,0 +1,172 @@ + + + + + + + + testimate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + help button + +
+
+ +
+
+
+
drop attribute here
+
+ outcome/primary attribute + + 🗑 +
+
+
+ +
+
+
drop attribute here
+
+ predictor/secondary attribute + + 🗑 +
+
+
+ +
+ +
testID header
+
results go here
+
+ + + + + % +
+ + + +
+
+ Emit data into CODAP +
+ +
+ + + +
+ +
+ + + + + +
+ +
+ + + +
+ +
+
+
+ +
+ + + + + + diff --git a/eepsmedia/plugins/testimate/jstat/jstat.js b/eepsmedia/plugins/testimate/jstat/jstat.js new file mode 100644 index 00000000..13feb888 --- /dev/null +++ b/eepsmedia/plugins/testimate/jstat/jstat.js @@ -0,0 +1,4999 @@ +(function (window, factory) { + if (typeof exports === 'object') { + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + define(factory); + } else { + window.jStat = factory(); + } +})(this, function () { +var jStat = (function(Math, undefined) { + +// For quick reference. +var concat = Array.prototype.concat; +var slice = Array.prototype.slice; +var toString = Object.prototype.toString; + +// Calculate correction for IEEE error +// TODO: This calculation can be improved. +function calcRdx(n, m) { + var val = n > m ? n : m; + return Math.pow(10, + 17 - ~~(Math.log(((val > 0) ? val : -val)) * Math.LOG10E)); +} + + +var isArray = Array.isArray || function isArray(arg) { + return toString.call(arg) === '[object Array]'; +}; + + +function isFunction(arg) { + return toString.call(arg) === '[object Function]'; +} + + +function isNumber(num) { + return (typeof num === 'number') ? num - num === 0 : false; +} + + +// Converts the jStat matrix to vector. +function toVector(arr) { + return concat.apply([], arr); +} + + +// The one and only jStat constructor. +function jStat() { + return new jStat._init(arguments); +} + + +// TODO: Remove after all references in src files have been removed. +jStat.fn = jStat.prototype; + + +// By separating the initializer from the constructor it's easier to handle +// always returning a new instance whether "new" was used or not. +jStat._init = function _init(args) { + // If first argument is an array, must be vector or matrix. + if (isArray(args[0])) { + // Check if matrix. + if (isArray(args[0][0])) { + // See if a mapping function was also passed. + if (isFunction(args[1])) + args[0] = jStat.map(args[0], args[1]); + // Iterate over each is faster than this.push.apply(this, args[0]. + for (var i = 0; i < args[0].length; i++) + this[i] = args[0][i]; + this.length = args[0].length; + + // Otherwise must be a vector. + } else { + this[0] = isFunction(args[1]) ? jStat.map(args[0], args[1]) : args[0]; + this.length = 1; + } + + // If first argument is number, assume creation of sequence. + } else if (isNumber(args[0])) { + this[0] = jStat.seq.apply(null, args); + this.length = 1; + + // Handle case when jStat object is passed to jStat. + } else if (args[0] instanceof jStat) { + // Duplicate the object and pass it back. + return jStat(args[0].toArray()); + + // Unexpected argument value, return empty jStat object. + // TODO: This is strange behavior. Shouldn't this throw or some such to let + // the user know they had bad arguments? + } else { + this[0] = []; + this.length = 1; + } + + return this; +}; +jStat._init.prototype = jStat.prototype; +jStat._init.constructor = jStat; + + +// Utility functions. +// TODO: for internal use only? +jStat.utils = { + calcRdx: calcRdx, + isArray: isArray, + isFunction: isFunction, + isNumber: isNumber, + toVector: toVector +}; + + +jStat._random_fn = Math.random; +jStat.setRandom = function setRandom(fn) { + if (typeof fn !== 'function') + throw new TypeError('fn is not a function'); + jStat._random_fn = fn; +}; + + +// Easily extend the jStat object. +// TODO: is this seriously necessary? +jStat.extend = function extend(obj) { + var i, j; + + if (arguments.length === 1) { + for (j in obj) + jStat[j] = obj[j]; + return this; + } + + for (i = 1; i < arguments.length; i++) { + for (j in arguments[i]) + obj[j] = arguments[i][j]; + } + + return obj; +}; + + +// Returns the number of rows in the matrix. +jStat.rows = function rows(arr) { + return arr.length || 1; +}; + + +// Returns the number of columns in the matrix. +jStat.cols = function cols(arr) { + return arr[0].length || 1; +}; + + +// Returns the dimensions of the object { rows: i, cols: j } +jStat.dimensions = function dimensions(arr) { + return { + rows: jStat.rows(arr), + cols: jStat.cols(arr) + }; +}; + + +// Returns a specified row as a vector or return a sub matrix by pick some rows +jStat.row = function row(arr, index) { + if (isArray(index)) { + return index.map(function(i) { + return jStat.row(arr, i); + }) + } + return arr[index]; +}; + + +// return row as array +// rowa([[1,2],[3,4]],0) -> [1,2] +jStat.rowa = function rowa(arr, i) { + return jStat.row(arr, i); +}; + + +// Returns the specified column as a vector or return a sub matrix by pick some +// columns +jStat.col = function col(arr, index) { + if (isArray(index)) { + var submat = jStat.arange(arr.length).map(function() { + return new Array(index.length); + }); + index.forEach(function(ind, i){ + jStat.arange(arr.length).forEach(function(j) { + submat[j][i] = arr[j][ind]; + }); + }); + return submat; + } + var column = new Array(arr.length); + for (var i = 0; i < arr.length; i++) + column[i] = [arr[i][index]]; + return column; +}; + + +// return column as array +// cola([[1,2],[3,4]],0) -> [1,3] +jStat.cola = function cola(arr, i) { + return jStat.col(arr, i).map(function(a){ return a[0] }); +}; + + +// Returns the diagonal of the matrix +jStat.diag = function diag(arr) { + var nrow = jStat.rows(arr); + var res = new Array(nrow); + for (var row = 0; row < nrow; row++) + res[row] = [arr[row][row]]; + return res; +}; + + +// Returns the anti-diagonal of the matrix +jStat.antidiag = function antidiag(arr) { + var nrow = jStat.rows(arr) - 1; + var res = new Array(nrow); + for (var i = 0; nrow >= 0; nrow--, i++) + res[i] = [arr[i][nrow]]; + return res; +}; + +// Transpose a matrix or array. +jStat.transpose = function transpose(arr) { + var obj = []; + var objArr, rows, cols, j, i; + + // Make sure arr is in matrix format. + if (!isArray(arr[0])) + arr = [arr]; + + rows = arr.length; + cols = arr[0].length; + + for (i = 0; i < cols; i++) { + objArr = new Array(rows); + for (j = 0; j < rows; j++) + objArr[j] = arr[j][i]; + obj.push(objArr); + } + + // If obj is vector, return only single array. + return obj.length === 1 ? obj[0] : obj; +}; + + +// Map a function to an array or array of arrays. +// "toAlter" is an internal variable. +jStat.map = function map(arr, func, toAlter) { + var row, nrow, ncol, res, col; + + if (!isArray(arr[0])) + arr = [arr]; + + nrow = arr.length; + ncol = arr[0].length; + res = toAlter ? arr : new Array(nrow); + + for (row = 0; row < nrow; row++) { + // if the row doesn't exist, create it + if (!res[row]) + res[row] = new Array(ncol); + for (col = 0; col < ncol; col++) + res[row][col] = func(arr[row][col], row, col); + } + + return res.length === 1 ? res[0] : res; +}; + + +// Cumulatively combine the elements of an array or array of arrays using a function. +jStat.cumreduce = function cumreduce(arr, func, toAlter) { + var row, nrow, ncol, res, col; + + if (!isArray(arr[0])) + arr = [arr]; + + nrow = arr.length; + ncol = arr[0].length; + res = toAlter ? arr : new Array(nrow); + + for (row = 0; row < nrow; row++) { + // if the row doesn't exist, create it + if (!res[row]) + res[row] = new Array(ncol); + if (ncol > 0) + res[row][0] = arr[row][0]; + for (col = 1; col < ncol; col++) + res[row][col] = func(res[row][col-1], arr[row][col]); + } + return res.length === 1 ? res[0] : res; +}; + + +// Destructively alter an array. +jStat.alter = function alter(arr, func) { + return jStat.map(arr, func, true); +}; + + +// Generate a rows x cols matrix according to the supplied function. +jStat.create = function create(rows, cols, func) { + var res = new Array(rows); + var i, j; + + if (isFunction(cols)) { + func = cols; + cols = rows; + } + + for (i = 0; i < rows; i++) { + res[i] = new Array(cols); + for (j = 0; j < cols; j++) + res[i][j] = func(i, j); + } + + return res; +}; + + +function retZero() { return 0; } + + +// Generate a rows x cols matrix of zeros. +jStat.zeros = function zeros(rows, cols) { + if (!isNumber(cols)) + cols = rows; + return jStat.create(rows, cols, retZero); +}; + + +function retOne() { return 1; } + + +// Generate a rows x cols matrix of ones. +jStat.ones = function ones(rows, cols) { + if (!isNumber(cols)) + cols = rows; + return jStat.create(rows, cols, retOne); +}; + + +// Generate a rows x cols matrix of uniformly random numbers. +jStat.rand = function rand(rows, cols) { + if (!isNumber(cols)) + cols = rows; + return jStat.create(rows, cols, jStat._random_fn); +}; + + +function retIdent(i, j) { return i === j ? 1 : 0; } + + +// Generate an identity matrix of size row x cols. +jStat.identity = function identity(rows, cols) { + if (!isNumber(cols)) + cols = rows; + return jStat.create(rows, cols, retIdent); +}; + + +// Tests whether a matrix is symmetric +jStat.symmetric = function symmetric(arr) { + var size = arr.length; + var row, col; + + if (arr.length !== arr[0].length) + return false; + + for (row = 0; row < size; row++) { + for (col = 0; col < size; col++) + if (arr[col][row] !== arr[row][col]) + return false; + } + + return true; +}; + + +// Set all values to zero. +jStat.clear = function clear(arr) { + return jStat.alter(arr, retZero); +}; + + +// Generate sequence. +jStat.seq = function seq(min, max, length, func) { + if (!isFunction(func)) + func = false; + + var arr = []; + var hival = calcRdx(min, max); + var step = (max * hival - min * hival) / ((length - 1) * hival); + var current = min; + var cnt; + + // Current is assigned using a technique to compensate for IEEE error. + // TODO: Needs better implementation. + for (cnt = 0; + current <= max && cnt < length; + cnt++, current = (min * hival + step * hival * cnt) / hival) { + arr.push((func ? func(current, cnt) : current)); + } + + return arr; +}; + + +// arange(5) -> [0,1,2,3,4] +// arange(1,5) -> [1,2,3,4] +// arange(5,1,-1) -> [5,4,3,2] +jStat.arange = function arange(start, end, step) { + var rl = []; + var i; + step = step || 1; + if (end === undefined) { + end = start; + start = 0; + } + if (start === end || step === 0) { + return []; + } + if (start < end && step < 0) { + return []; + } + if (start > end && step > 0) { + return []; + } + if (step > 0) { + for (i = start; i < end; i += step) { + rl.push(i); + } + } else { + for (i = start; i > end; i += step) { + rl.push(i); + } + } + return rl; +}; + + +// A=[[1,2,3],[4,5,6],[7,8,9]] +// slice(A,{row:{end:2},col:{start:1}}) -> [[2,3],[5,6]] +// slice(A,1,{start:1}) -> [5,6] +// as numpy code A[:2,1:] +jStat.slice = (function(){ + function _slice(list, start, end, step) { + // note it's not equal to range.map mode it's a bug + var i; + var rl = []; + var length = list.length; + if (start === undefined && end === undefined && step === undefined) { + return jStat.copy(list); + } + + start = start || 0; + end = end || list.length; + start = start >= 0 ? start : length + start; + end = end >= 0 ? end : length + end; + step = step || 1; + if (start === end || step === 0) { + return []; + } + if (start < end && step < 0) { + return []; + } + if (start > end && step > 0) { + return []; + } + if (step > 0) { + for (i = start; i < end; i += step) { + rl.push(list[i]); + } + } else { + for (i = start; i > end;i += step) { + rl.push(list[i]); + } + } + return rl; + } + + function slice(list, rcSlice) { + var colSlice, rowSlice; + rcSlice = rcSlice || {}; + if (isNumber(rcSlice.row)) { + if (isNumber(rcSlice.col)) + return list[rcSlice.row][rcSlice.col]; + var row = jStat.rowa(list, rcSlice.row); + colSlice = rcSlice.col || {}; + return _slice(row, colSlice.start, colSlice.end, colSlice.step); + } + + if (isNumber(rcSlice.col)) { + var col = jStat.cola(list, rcSlice.col); + rowSlice = rcSlice.row || {}; + return _slice(col, rowSlice.start, rowSlice.end, rowSlice.step); + } + + rowSlice = rcSlice.row || {}; + colSlice = rcSlice.col || {}; + var rows = _slice(list, rowSlice.start, rowSlice.end, rowSlice.step); + return rows.map(function(row) { + return _slice(row, colSlice.start, colSlice.end, colSlice.step); + }); + } + + return slice; +}()); + + +// A=[[1,2,3],[4,5,6],[7,8,9]] +// sliceAssign(A,{row:{start:1},col:{start:1}},[[0,0],[0,0]]) +// A=[[1,2,3],[4,0,0],[7,0,0]] +jStat.sliceAssign = function sliceAssign(A, rcSlice, B) { + var nl, ml; + if (isNumber(rcSlice.row)) { + if (isNumber(rcSlice.col)) + return A[rcSlice.row][rcSlice.col] = B; + rcSlice.col = rcSlice.col || {}; + rcSlice.col.start = rcSlice.col.start || 0; + rcSlice.col.end = rcSlice.col.end || A[0].length; + rcSlice.col.step = rcSlice.col.step || 1; + nl = jStat.arange(rcSlice.col.start, + Math.min(A.length, rcSlice.col.end), + rcSlice.col.step); + var m = rcSlice.row; + nl.forEach(function(n, i) { + A[m][n] = B[i]; + }); + return A; + } + + if (isNumber(rcSlice.col)) { + rcSlice.row = rcSlice.row || {}; + rcSlice.row.start = rcSlice.row.start || 0; + rcSlice.row.end = rcSlice.row.end || A.length; + rcSlice.row.step = rcSlice.row.step || 1; + ml = jStat.arange(rcSlice.row.start, + Math.min(A[0].length, rcSlice.row.end), + rcSlice.row.step); + var n = rcSlice.col; + ml.forEach(function(m, j) { + A[m][n] = B[j]; + }); + return A; + } + + if (B[0].length === undefined) { + B = [B]; + } + rcSlice.row.start = rcSlice.row.start || 0; + rcSlice.row.end = rcSlice.row.end || A.length; + rcSlice.row.step = rcSlice.row.step || 1; + rcSlice.col.start = rcSlice.col.start || 0; + rcSlice.col.end = rcSlice.col.end || A[0].length; + rcSlice.col.step = rcSlice.col.step || 1; + ml = jStat.arange(rcSlice.row.start, + Math.min(A.length, rcSlice.row.end), + rcSlice.row.step); + nl = jStat.arange(rcSlice.col.start, + Math.min(A[0].length, rcSlice.col.end), + rcSlice.col.step); + ml.forEach(function(m, i) { + nl.forEach(function(n, j) { + A[m][n] = B[i][j]; + }); + }); + return A; +}; + + +// [1,2,3] -> +// [[1,0,0],[0,2,0],[0,0,3]] +jStat.diagonal = function diagonal(diagArray) { + var mat = jStat.zeros(diagArray.length, diagArray.length); + diagArray.forEach(function(t, i) { + mat[i][i] = t; + }); + return mat; +}; + + +// return copy of A +jStat.copy = function copy(A) { + return A.map(function(row) { + if (isNumber(row)) + return row; + return row.map(function(t) { + return t; + }); + }); +}; + + +// TODO: Go over this entire implementation. Seems a tragic waste of resources +// doing all this work. Instead, and while ugly, use new Function() to generate +// a custom function for each static method. + +// Quick reference. +var jProto = jStat.prototype; + +// Default length. +jProto.length = 0; + +// For internal use only. +// TODO: Check if they're actually used, and if they are then rename them +// to _* +jProto.push = Array.prototype.push; +jProto.sort = Array.prototype.sort; +jProto.splice = Array.prototype.splice; +jProto.slice = Array.prototype.slice; + + +// Return a clean array. +jProto.toArray = function toArray() { + return this.length > 1 ? slice.call(this) : slice.call(this)[0]; +}; + + +// Map a function to a matrix or vector. +jProto.map = function map(func, toAlter) { + return jStat(jStat.map(this, func, toAlter)); +}; + + +// Cumulatively combine the elements of a matrix or vector using a function. +jProto.cumreduce = function cumreduce(func, toAlter) { + return jStat(jStat.cumreduce(this, func, toAlter)); +}; + + +// Destructively alter an array. +jProto.alter = function alter(func) { + jStat.alter(this, func); + return this; +}; + + +// Extend prototype with methods that have no argument. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jProto[passfunc] = function(func) { + var self = this, + results; + // Check for callback. + if (func) { + setTimeout(function() { + func.call(self, jProto[passfunc].call(self)); + }); + return this; + } + results = jStat[passfunc](this); + return isArray(results) ? jStat(results) : results; + }; + })(funcs[i]); +})('transpose clear symmetric rows cols dimensions diag antidiag'.split(' ')); + + +// Extend prototype with methods that have one argument. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jProto[passfunc] = function(index, func) { + var self = this; + // check for callback + if (func) { + setTimeout(function() { + func.call(self, jProto[passfunc].call(self, index)); + }); + return this; + } + return jStat(jStat[passfunc](this, index)); + }; + })(funcs[i]); +})('row col'.split(' ')); + + +// Extend prototype with simple shortcut methods. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jProto[passfunc] = function() { + return jStat(jStat[passfunc].apply(null, arguments)); + }; + })(funcs[i]); +})('create zeros ones rand identity'.split(' ')); + + +// Exposing jStat. +return jStat; + +}(Math)); +(function(jStat, Math) { + +var isFunction = jStat.utils.isFunction; + +// Ascending functions for sort +function ascNum(a, b) { return a - b; } + +function clip(arg, min, max) { + return Math.max(min, Math.min(arg, max)); +} + + +// sum of an array +jStat.sum = function sum(arr) { + var sum = 0; + var i = arr.length; + while (--i >= 0) + sum += arr[i]; + return sum; +}; + + +// sum squared +jStat.sumsqrd = function sumsqrd(arr) { + var sum = 0; + var i = arr.length; + while (--i >= 0) + sum += arr[i] * arr[i]; + return sum; +}; + + +// sum of squared errors of prediction (SSE) +jStat.sumsqerr = function sumsqerr(arr) { + var mean = jStat.mean(arr); + var sum = 0; + var i = arr.length; + var tmp; + while (--i >= 0) { + tmp = arr[i] - mean; + sum += tmp * tmp; + } + return sum; +}; + +// sum of an array in each row +jStat.sumrow = function sumrow(arr) { + var sum = 0; + var i = arr.length; + while (--i >= 0) + sum += arr[i]; + return sum; +}; + +// product of an array +jStat.product = function product(arr) { + var prod = 1; + var i = arr.length; + while (--i >= 0) + prod *= arr[i]; + return prod; +}; + + +// minimum value of an array +jStat.min = function min(arr) { + var low = arr[0]; + var i = 0; + while (++i < arr.length) + if (arr[i] < low) + low = arr[i]; + return low; +}; + + +// maximum value of an array +jStat.max = function max(arr) { + var high = arr[0]; + var i = 0; + while (++i < arr.length) + if (arr[i] > high) + high = arr[i]; + return high; +}; + + +// unique values of an array +jStat.unique = function unique(arr) { + var hash = {}, _arr = []; + for(var i = 0; i < arr.length; i++) { + if (!hash[arr[i]]) { + hash[arr[i]] = true; + _arr.push(arr[i]); + } + } + return _arr; +}; + + +// mean value of an array +jStat.mean = function mean(arr) { + return jStat.sum(arr) / arr.length; +}; + + +// mean squared error (MSE) +jStat.meansqerr = function meansqerr(arr) { + return jStat.sumsqerr(arr) / arr.length; +}; + + +// geometric mean of an array +jStat.geomean = function geomean(arr) { + var logs = arr.map(Math.log) + var meanOfLogs = jStat.mean(logs) + return Math.exp(meanOfLogs) +}; + + +// median of an array +jStat.median = function median(arr) { + var arrlen = arr.length; + var _arr = arr.slice().sort(ascNum); + // check if array is even or odd, then return the appropriate + return !(arrlen & 1) + ? (_arr[(arrlen / 2) - 1 ] + _arr[(arrlen / 2)]) / 2 + : _arr[(arrlen / 2) | 0 ]; +}; + + +// cumulative sum of an array +jStat.cumsum = function cumsum(arr) { + return jStat.cumreduce(arr, function (a, b) { return a + b; }); +}; + + +// cumulative product of an array +jStat.cumprod = function cumprod(arr) { + return jStat.cumreduce(arr, function (a, b) { return a * b; }); +}; + + +// successive differences of a sequence +jStat.diff = function diff(arr) { + var diffs = []; + var arrLen = arr.length; + var i; + for (i = 1; i < arrLen; i++) + diffs.push(arr[i] - arr[i - 1]); + return diffs; +}; + + +// ranks of an array +jStat.rank = function (arr) { + var i; + var distinctNumbers = []; + var numberCounts = {}; + for (i = 0; i < arr.length; i++) { + var number = arr[i]; + if (numberCounts[number]) { + numberCounts[number]++; + } else { + numberCounts[number] = 1; + distinctNumbers.push(number); + } + } + + var sortedDistinctNumbers = distinctNumbers.sort(ascNum); + var numberRanks = {}; + var currentRank = 1; + for (i = 0; i < sortedDistinctNumbers.length; i++) { + var number = sortedDistinctNumbers[i]; + var count = numberCounts[number]; + var first = currentRank; + var last = currentRank + count - 1; + var rank = (first + last) / 2; + numberRanks[number] = rank; + currentRank += count; + } + + return arr.map(function (number) { + return numberRanks[number]; + }); +}; + + +// mode of an array +// if there are multiple modes of an array, return all of them +// is this the appropriate way of handling it? +jStat.mode = function mode(arr) { + var arrLen = arr.length; + var _arr = arr.slice().sort(ascNum); + var count = 1; + var maxCount = 0; + var numMaxCount = 0; + var mode_arr = []; + var i; + + for (i = 0; i < arrLen; i++) { + if (_arr[i] === _arr[i + 1]) { + count++; + } else { + if (count > maxCount) { + mode_arr = [_arr[i]]; + maxCount = count; + numMaxCount = 0; + } + // are there multiple max counts + else if (count === maxCount) { + mode_arr.push(_arr[i]); + numMaxCount++; + } + // resetting count for new value in array + count = 1; + } + } + + return numMaxCount === 0 ? mode_arr[0] : mode_arr; +}; + + +// range of an array +jStat.range = function range(arr) { + return jStat.max(arr) - jStat.min(arr); +}; + +// variance of an array +// flag = true indicates sample instead of population +jStat.variance = function variance(arr, flag) { + return jStat.sumsqerr(arr) / (arr.length - (flag ? 1 : 0)); +}; + +// pooled variance of an array of arrays +jStat.pooledvariance = function pooledvariance(arr) { + var sumsqerr = arr.reduce(function (a, samples) {return a + jStat.sumsqerr(samples);}, 0); + var count = arr.reduce(function (a, samples) {return a + samples.length;}, 0); + return sumsqerr / (count - arr.length); +}; + +// deviation of an array +jStat.deviation = function (arr) { + var mean = jStat.mean(arr); + var arrlen = arr.length; + var dev = new Array(arrlen); + for (var i = 0; i < arrlen; i++) { + dev[i] = arr[i] - mean; + } + return dev; +}; + +// standard deviation of an array +// flag = true indicates sample instead of population +jStat.stdev = function stdev(arr, flag) { + return Math.sqrt(jStat.variance(arr, flag)); +}; + +// pooled standard deviation of an array of arrays +jStat.pooledstdev = function pooledstdev(arr) { + return Math.sqrt(jStat.pooledvariance(arr)); +}; + +// mean deviation (mean absolute deviation) of an array +jStat.meandev = function meandev(arr) { + var mean = jStat.mean(arr); + var a = []; + for (var i = arr.length - 1; i >= 0; i--) { + a.push(Math.abs(arr[i] - mean)); + } + return jStat.mean(a); +}; + + +// median deviation (median absolute deviation) of an array +jStat.meddev = function meddev(arr) { + var median = jStat.median(arr); + var a = []; + for (var i = arr.length - 1; i >= 0; i--) { + a.push(Math.abs(arr[i] - median)); + } + return jStat.median(a); +}; + + +// coefficient of variation +jStat.coeffvar = function coeffvar(arr) { + return jStat.stdev(arr) / jStat.mean(arr); +}; + + +// quartiles of an array +jStat.quartiles = function quartiles(arr) { + var arrlen = arr.length; + var _arr = arr.slice().sort(ascNum); + return [ + _arr[ Math.round((arrlen) / 4) - 1 ], + _arr[ Math.round((arrlen) / 2) - 1 ], + _arr[ Math.round((arrlen) * 3 / 4) - 1 ] + ]; +}; + + +// Arbitary quantiles of an array. Direct port of the scipy.stats +// implementation by Pierre GF Gerard-Marchant. +jStat.quantiles = function quantiles(arr, quantilesArray, alphap, betap) { + var sortedArray = arr.slice().sort(ascNum); + var quantileVals = [quantilesArray.length]; + var n = arr.length; + var i, p, m, aleph, k, gamma; + + if (typeof alphap === 'undefined') + alphap = 3 / 8; + if (typeof betap === 'undefined') + betap = 3 / 8; + + for (i = 0; i < quantilesArray.length; i++) { + p = quantilesArray[i]; + m = alphap + p * (1 - alphap - betap); + aleph = n * p + m; + k = Math.floor(clip(aleph, 1, n - 1)); + gamma = clip(aleph - k, 0, 1); + quantileVals[i] = (1 - gamma) * sortedArray[k - 1] + gamma * sortedArray[k]; + } + + return quantileVals; +}; + +// Return the k-th percentile of values in a range, where k is in the range 0..1, inclusive. +// Passing true for the exclusive parameter excludes both endpoints of the range. +jStat.percentile = function percentile(arr, k, exclusive) { + var _arr = arr.slice().sort(ascNum); + var realIndex = k * (_arr.length + (exclusive ? 1 : -1)) + (exclusive ? 0 : 1); + var index = parseInt(realIndex); + var frac = realIndex - index; + if (index + 1 < _arr.length) { + return _arr[index - 1] + frac * (_arr[index] - _arr[index - 1]); + } else { + return _arr[index - 1]; + } +} + +// The percentile rank of score in a given array. Returns the percentage +// of all values in the input array that are less than (kind='strict') or +// less or equal than (kind='weak') score. Default is weak. +jStat.percentileOfScore = function percentileOfScore(arr, score, kind) { + var counter = 0; + var len = arr.length; + var strict = false; + var value, i; + + if (kind === 'strict') + strict = true; + + for (i = 0; i < len; i++) { + value = arr[i]; + if ((strict && value < score) || + (!strict && value <= score)) { + counter++; + } + } + + return counter / len; +}; + + +// Histogram (bin count) data +jStat.histogram = function histogram(arr, binCnt) { + binCnt = binCnt || 4; + var first = jStat.min(arr); + var binWidth = (jStat.max(arr) - first) / binCnt; + var len = arr.length; + var bins = []; + var i; + + for (i = 0; i < binCnt; i++) + bins[i] = 0; + for (i = 0; i < len; i++) + bins[Math.min(Math.floor(((arr[i] - first) / binWidth)), binCnt - 1)] += 1; + + return bins; +}; + + +// covariance of two arrays +jStat.covariance = function covariance(arr1, arr2) { + var u = jStat.mean(arr1); + var v = jStat.mean(arr2); + var arr1Len = arr1.length; + var sq_dev = new Array(arr1Len); + var i; + + for (i = 0; i < arr1Len; i++) + sq_dev[i] = (arr1[i] - u) * (arr2[i] - v); + + return jStat.sum(sq_dev) / (arr1Len - 1); +}; + + +// (pearson's) population correlation coefficient, rho +jStat.corrcoeff = function corrcoeff(arr1, arr2) { + return jStat.covariance(arr1, arr2) / + jStat.stdev(arr1, 1) / + jStat.stdev(arr2, 1); +}; + + // (spearman's) rank correlation coefficient, sp +jStat.spearmancoeff = function (arr1, arr2) { + arr1 = jStat.rank(arr1); + arr2 = jStat.rank(arr2); + //return pearson's correlation of the ranks: + return jStat.corrcoeff(arr1, arr2); +} + + +// statistical standardized moments (general form of skew/kurt) +jStat.stanMoment = function stanMoment(arr, n) { + var mu = jStat.mean(arr); + var sigma = jStat.stdev(arr); + var len = arr.length; + var skewSum = 0; + + for (var i = 0; i < len; i++) + skewSum += Math.pow((arr[i] - mu) / sigma, n); + + return skewSum / arr.length; +}; + +// (pearson's) moment coefficient of skewness +jStat.skewness = function skewness(arr) { + return jStat.stanMoment(arr, 3); +}; + +// (pearson's) (excess) kurtosis +jStat.kurtosis = function kurtosis(arr) { + return jStat.stanMoment(arr, 4) - 3; +}; + + +var jProto = jStat.prototype; + + +// Extend jProto with method for calculating cumulative sums and products. +// This differs from the similar extension below as cumsum and cumprod should +// not be run again in the case fullbool === true. +// If a matrix is passed, automatically assume operation should be done on the +// columns. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + // If a matrix is passed, automatically assume operation should be done on + // the columns. + jProto[passfunc] = function(fullbool, func) { + var arr = []; + var i = 0; + var tmpthis = this; + // Assignment reassignation depending on how parameters were passed in. + if (isFunction(fullbool)) { + func = fullbool; + fullbool = false; + } + // Check if a callback was passed with the function. + if (func) { + setTimeout(function() { + func.call(tmpthis, jProto[passfunc].call(tmpthis, fullbool)); + }); + return this; + } + // Check if matrix and run calculations. + if (this.length > 1) { + tmpthis = fullbool === true ? this : this.transpose(); + for (; i < tmpthis.length; i++) + arr[i] = jStat[passfunc](tmpthis[i]); + return arr; + } + // Pass fullbool if only vector, not a matrix. for variance and stdev. + return jStat[passfunc](this[0], fullbool); + }; + })(funcs[i]); +})(('cumsum cumprod').split(' ')); + + +// Extend jProto with methods which don't require arguments and work on columns. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + // If a matrix is passed, automatically assume operation should be done on + // the columns. + jProto[passfunc] = function(fullbool, func) { + var arr = []; + var i = 0; + var tmpthis = this; + // Assignment reassignation depending on how parameters were passed in. + if (isFunction(fullbool)) { + func = fullbool; + fullbool = false; + } + // Check if a callback was passed with the function. + if (func) { + setTimeout(function() { + func.call(tmpthis, jProto[passfunc].call(tmpthis, fullbool)); + }); + return this; + } + // Check if matrix and run calculations. + if (this.length > 1) { + if (passfunc !== 'sumrow') + tmpthis = fullbool === true ? this : this.transpose(); + for (; i < tmpthis.length; i++) + arr[i] = jStat[passfunc](tmpthis[i]); + return fullbool === true + ? jStat[passfunc](jStat.utils.toVector(arr)) + : arr; + } + // Pass fullbool if only vector, not a matrix. for variance and stdev. + return jStat[passfunc](this[0], fullbool); + }; + })(funcs[i]); +})(('sum sumsqrd sumsqerr sumrow product min max unique mean meansqerr ' + + 'geomean median diff rank mode range variance deviation stdev meandev ' + + 'meddev coeffvar quartiles histogram skewness kurtosis').split(' ')); + + +// Extend jProto with functions that take arguments. Operations on matrices are +// done on columns. +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jProto[passfunc] = function() { + var arr = []; + var i = 0; + var tmpthis = this; + var args = Array.prototype.slice.call(arguments); + var callbackFunction; + + // If the last argument is a function, we assume it's a callback; we + // strip the callback out and call the function again. + if (isFunction(args[args.length - 1])) { + callbackFunction = args[args.length - 1]; + var argsToPass = args.slice(0, args.length - 1); + + setTimeout(function() { + callbackFunction.call(tmpthis, + jProto[passfunc].apply(tmpthis, argsToPass)); + }); + return this; + + // Otherwise we curry the function args and call normally. + } else { + callbackFunction = undefined; + var curriedFunction = function curriedFunction(vector) { + return jStat[passfunc].apply(tmpthis, [vector].concat(args)); + } + } + + // If this is a matrix, run column-by-column. + if (this.length > 1) { + tmpthis = tmpthis.transpose(); + for (; i < tmpthis.length; i++) + arr[i] = curriedFunction(tmpthis[i]); + return arr; + } + + // Otherwise run on the vector. + return curriedFunction(this[0]); + }; + })(funcs[i]); +})('quantiles percentileOfScore'.split(' ')); + +}(jStat, Math)); +// Special functions // +(function(jStat, Math) { + +// Log-gamma function +jStat.gammaln = function gammaln(x) { + var j = 0; + var cof = [ + 76.18009172947146, -86.50532032941677, 24.01409824083091, + -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5 + ]; + var ser = 1.000000000190015; + var xx, y, tmp; + tmp = (y = xx = x) + 5.5; + tmp -= (xx + 0.5) * Math.log(tmp); + for (; j < 6; j++) + ser += cof[j] / ++y; + return Math.log(2.5066282746310005 * ser / xx) - tmp; +}; + +/* + * log-gamma function to support poisson distribution sampling. The + * algorithm comes from SPECFUN by Shanjie Zhang and Jianming Jin and their + * book "Computation of Special Functions", 1996, John Wiley & Sons, Inc. + */ +jStat.loggam = function loggam(x) { + var x0, x2, xp, gl, gl0; + var k, n; + + var a = [8.333333333333333e-02, -2.777777777777778e-03, + 7.936507936507937e-04, -5.952380952380952e-04, + 8.417508417508418e-04, -1.917526917526918e-03, + 6.410256410256410e-03, -2.955065359477124e-02, + 1.796443723688307e-01, -1.39243221690590e+00]; + x0 = x; + n = 0; + if ((x == 1.0) || (x == 2.0)) { + return 0.0; + } + if (x <= 7.0) { + n = Math.floor(7 - x); + x0 = x + n; + } + x2 = 1.0 / (x0 * x0); + xp = 2 * Math.PI; + gl0 = a[9]; + for (k = 8; k >= 0; k--) { + gl0 *= x2; + gl0 += a[k]; + } + gl = gl0 / x0 + 0.5 * Math.log(xp) + (x0 - 0.5) * Math.log(x0) - x0; + if (x <= 7.0) { + for (k = 1; k <= n; k++) { + gl -= Math.log(x0 - 1.0); + x0 -= 1.0; + } + } + return gl; +} + +// gamma of x +jStat.gammafn = function gammafn(x) { + var p = [-1.716185138865495, 24.76565080557592, -379.80425647094563, + 629.3311553128184, 866.9662027904133, -31451.272968848367, + -36144.413418691176, 66456.14382024054 + ]; + var q = [-30.8402300119739, 315.35062697960416, -1015.1563674902192, + -3107.771671572311, 22538.118420980151, 4755.8462775278811, + -134659.9598649693, -115132.2596755535]; + var fact = false; + var n = 0; + var xden = 0; + var xnum = 0; + var y = x; + var i, z, yi, res; + if (x > 171.6243769536076) { + return Infinity; + } + if (y <= 0) { + res = y % 1 + 3.6e-16; + if (res) { + fact = (!(y & 1) ? 1 : -1) * Math.PI / Math.sin(Math.PI * res); + y = 1 - y; + } else { + return Infinity; + } + } + yi = y; + if (y < 1) { + z = y++; + } else { + z = (y -= n = (y | 0) - 1) - 1; + } + for (i = 0; i < 8; ++i) { + xnum = (xnum + p[i]) * z; + xden = xden * z + q[i]; + } + res = xnum / xden + 1; + if (yi < y) { + res /= yi; + } else if (yi > y) { + for (i = 0; i < n; ++i) { + res *= y; + y++; + } + } + if (fact) { + res = fact / res; + } + return res; +}; + + +// lower incomplete gamma function, which is usually typeset with a +// lower-case greek gamma as the function symbol +jStat.gammap = function gammap(a, x) { + return jStat.lowRegGamma(a, x) * jStat.gammafn(a); +}; + + +// The lower regularized incomplete gamma function, usually written P(a,x) +jStat.lowRegGamma = function lowRegGamma(a, x) { + var aln = jStat.gammaln(a); + var ap = a; + var sum = 1 / a; + var del = sum; + var b = x + 1 - a; + var c = 1 / 1.0e-30; + var d = 1 / b; + var h = d; + var i = 1; + // calculate maximum number of itterations required for a + var ITMAX = -~(Math.log((a >= 1) ? a : 1 / a) * 8.5 + a * 0.4 + 17); + var an; + + if (x < 0 || a <= 0) { + return NaN; + } else if (x < a + 1) { + for (; i <= ITMAX; i++) { + sum += del *= x / ++ap; + } + return (sum * Math.exp(-x + a * Math.log(x) - (aln))); + } + + for (; i <= ITMAX; i++) { + an = -i * (i - a); + b += 2; + d = an * d + b; + c = b + an / c; + d = 1 / d; + h *= d * c; + } + + return (1 - h * Math.exp(-x + a * Math.log(x) - (aln))); +}; + +// natural log factorial of n +jStat.factorialln = function factorialln(n) { + return n < 0 ? NaN : jStat.gammaln(n + 1); +}; + +// factorial of n +jStat.factorial = function factorial(n) { + return n < 0 ? NaN : jStat.gammafn(n + 1); +}; + +// combinations of n, m +jStat.combination = function combination(n, m) { + // make sure n or m don't exceed the upper limit of usable values + return (n > 170 || m > 170) + ? Math.exp(jStat.combinationln(n, m)) + : (jStat.factorial(n) / jStat.factorial(m)) / jStat.factorial(n - m); +}; + + +jStat.combinationln = function combinationln(n, m){ + return jStat.factorialln(n) - jStat.factorialln(m) - jStat.factorialln(n - m); +}; + + +// permutations of n, m +jStat.permutation = function permutation(n, m) { + return jStat.factorial(n) / jStat.factorial(n - m); +}; + + +// beta function +jStat.betafn = function betafn(x, y) { + // ensure arguments are positive + if (x <= 0 || y <= 0) + return undefined; + // make sure x + y doesn't exceed the upper limit of usable values + return (x + y > 170) + ? Math.exp(jStat.betaln(x, y)) + : jStat.gammafn(x) * jStat.gammafn(y) / jStat.gammafn(x + y); +}; + + +// natural logarithm of beta function +jStat.betaln = function betaln(x, y) { + return jStat.gammaln(x) + jStat.gammaln(y) - jStat.gammaln(x + y); +}; + + +// Evaluates the continued fraction for incomplete beta function by modified +// Lentz's method. +jStat.betacf = function betacf(x, a, b) { + var fpmin = 1e-30; + var m = 1; + var qab = a + b; + var qap = a + 1; + var qam = a - 1; + var c = 1; + var d = 1 - qab * x / qap; + var m2, aa, del, h; + + // These q's will be used in factors that occur in the coefficients + if (Math.abs(d) < fpmin) + d = fpmin; + d = 1 / d; + h = d; + + for (; m <= 100; m++) { + m2 = 2 * m; + aa = m * (b - m) * x / ((qam + m2) * (a + m2)); + // One step (the even one) of the recurrence + d = 1 + aa * d; + if (Math.abs(d) < fpmin) + d = fpmin; + c = 1 + aa / c; + if (Math.abs(c) < fpmin) + c = fpmin; + d = 1 / d; + h *= d * c; + aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2)); + // Next step of the recurrence (the odd one) + d = 1 + aa * d; + if (Math.abs(d) < fpmin) + d = fpmin; + c = 1 + aa / c; + if (Math.abs(c) < fpmin) + c = fpmin; + d = 1 / d; + del = d * c; + h *= del; + if (Math.abs(del - 1.0) < 3e-7) + break; + } + + return h; +}; + + +// Returns the inverse of the lower regularized inomplete gamma function +jStat.gammapinv = function gammapinv(p, a) { + var j = 0; + var a1 = a - 1; + var EPS = 1e-8; + var gln = jStat.gammaln(a); + var x, err, t, u, pp, lna1, afac; + + if (p >= 1) + return Math.max(100, a + 100 * Math.sqrt(a)); + if (p <= 0) + return 0; + if (a > 1) { + lna1 = Math.log(a1); + afac = Math.exp(a1 * (lna1 - 1) - gln); + pp = (p < 0.5) ? p : 1 - p; + t = Math.sqrt(-2 * Math.log(pp)); + x = (2.30753 + t * 0.27061) / (1 + t * (0.99229 + t * 0.04481)) - t; + if (p < 0.5) + x = -x; + x = Math.max(1e-3, + a * Math.pow(1 - 1 / (9 * a) - x / (3 * Math.sqrt(a)), 3)); + } else { + t = 1 - a * (0.253 + a * 0.12); + if (p < t) + x = Math.pow(p / t, 1 / a); + else + x = 1 - Math.log(1 - (p - t) / (1 - t)); + } + + for(; j < 12; j++) { + if (x <= 0) + return 0; + err = jStat.lowRegGamma(a, x) - p; + if (a > 1) + t = afac * Math.exp(-(x - a1) + a1 * (Math.log(x) - lna1)); + else + t = Math.exp(-x + a1 * Math.log(x) - gln); + u = err / t; + x -= (t = u / (1 - 0.5 * Math.min(1, u * ((a - 1) / x - 1)))); + if (x <= 0) + x = 0.5 * (x + t); + if (Math.abs(t) < EPS * x) + break; + } + + return x; +}; + + +// Returns the error function erf(x) +jStat.erf = function erf(x) { + var cof = [-1.3026537197817094, 6.4196979235649026e-1, 1.9476473204185836e-2, + -9.561514786808631e-3, -9.46595344482036e-4, 3.66839497852761e-4, + 4.2523324806907e-5, -2.0278578112534e-5, -1.624290004647e-6, + 1.303655835580e-6, 1.5626441722e-8, -8.5238095915e-8, + 6.529054439e-9, 5.059343495e-9, -9.91364156e-10, + -2.27365122e-10, 9.6467911e-11, 2.394038e-12, + -6.886027e-12, 8.94487e-13, 3.13092e-13, + -1.12708e-13, 3.81e-16, 7.106e-15, + -1.523e-15, -9.4e-17, 1.21e-16, + -2.8e-17]; + var j = cof.length - 1; + var isneg = false; + var d = 0; + var dd = 0; + var t, ty, tmp, res; + + if (x < 0) { + x = -x; + isneg = true; + } + + t = 2 / (2 + x); + ty = 4 * t - 2; + + for(; j > 0; j--) { + tmp = d; + d = ty * d - dd + cof[j]; + dd = tmp; + } + + res = t * Math.exp(-x * x + 0.5 * (cof[0] + ty * d) - dd); + return isneg ? res - 1 : 1 - res; +}; + + +// Returns the complmentary error function erfc(x) +jStat.erfc = function erfc(x) { + return 1 - jStat.erf(x); +}; + + +// Returns the inverse of the complementary error function +jStat.erfcinv = function erfcinv(p) { + var j = 0; + var x, err, t, pp; + if (p >= 2) + return -100; + if (p <= 0) + return 100; + pp = (p < 1) ? p : 2 - p; + t = Math.sqrt(-2 * Math.log(pp / 2)); + x = -0.70711 * ((2.30753 + t * 0.27061) / + (1 + t * (0.99229 + t * 0.04481)) - t); + for (; j < 2; j++) { + err = jStat.erfc(x) - pp; + x += err / (1.12837916709551257 * Math.exp(-x * x) - x * err); + } + return (p < 1) ? x : -x; +}; + + +// Returns the inverse of the incomplete beta function +jStat.ibetainv = function ibetainv(p, a, b) { + var EPS = 1e-8; + var a1 = a - 1; + var b1 = b - 1; + var j = 0; + var lna, lnb, pp, t, u, err, x, al, h, w, afac; + if (p <= 0) + return 0; + if (p >= 1) + return 1; + if (a >= 1 && b >= 1) { + pp = (p < 0.5) ? p : 1 - p; + t = Math.sqrt(-2 * Math.log(pp)); + x = (2.30753 + t * 0.27061) / (1 + t* (0.99229 + t * 0.04481)) - t; + if (p < 0.5) + x = -x; + al = (x * x - 3) / 6; + h = 2 / (1 / (2 * a - 1) + 1 / (2 * b - 1)); + w = (x * Math.sqrt(al + h) / h) - (1 / (2 * b - 1) - 1 / (2 * a - 1)) * + (al + 5 / 6 - 2 / (3 * h)); + x = a / (a + b * Math.exp(2 * w)); + } else { + lna = Math.log(a / (a + b)); + lnb = Math.log(b / (a + b)); + t = Math.exp(a * lna) / a; + u = Math.exp(b * lnb) / b; + w = t + u; + if (p < t / w) + x = Math.pow(a * w * p, 1 / a); + else + x = 1 - Math.pow(b * w * (1 - p), 1 / b); + } + afac = -jStat.gammaln(a) - jStat.gammaln(b) + jStat.gammaln(a + b); + for(; j < 10; j++) { + if (x === 0 || x === 1) + return x; + err = jStat.ibeta(x, a, b) - p; + t = Math.exp(a1 * Math.log(x) + b1 * Math.log(1 - x) + afac); + u = err / t; + x -= (t = u / (1 - 0.5 * Math.min(1, u * (a1 / x - b1 / (1 - x))))); + if (x <= 0) + x = 0.5 * (x + t); + if (x >= 1) + x = 0.5 * (x + t + 1); + if (Math.abs(t) < EPS * x && j > 0) + break; + } + return x; +}; + + +// Returns the incomplete beta function I_x(a,b) +jStat.ibeta = function ibeta(x, a, b) { + // Factors in front of the continued fraction. + var bt = (x === 0 || x === 1) ? 0 : + Math.exp(jStat.gammaln(a + b) - jStat.gammaln(a) - + jStat.gammaln(b) + a * Math.log(x) + b * + Math.log(1 - x)); + if (x < 0 || x > 1) + return false; + if (x < (a + 1) / (a + b + 2)) + // Use continued fraction directly. + return bt * jStat.betacf(x, a, b) / a; + // else use continued fraction after making the symmetry transformation. + return 1 - bt * jStat.betacf(1 - x, b, a) / b; +}; + + +// Returns a normal deviate (mu=0, sigma=1). +// If n and m are specified it returns a object of normal deviates. +jStat.randn = function randn(n, m) { + var u, v, x, y, q; + if (!m) + m = n; + if (n) + return jStat.create(n, m, function() { return jStat.randn(); }); + do { + u = jStat._random_fn(); + v = 1.7156 * (jStat._random_fn() - 0.5); + x = u - 0.449871; + y = Math.abs(v) + 0.386595; + q = x * x + y * (0.19600 * y - 0.25472 * x); + } while (q > 0.27597 && (q > 0.27846 || v * v > -4 * Math.log(u) * u * u)); + return v / u; +}; + + +// Returns a gamma deviate by the method of Marsaglia and Tsang. +jStat.randg = function randg(shape, n, m) { + var oalph = shape; + var a1, a2, u, v, x, mat; + if (!m) + m = n; + if (!shape) + shape = 1; + if (n) { + mat = jStat.zeros(n,m); + mat.alter(function() { return jStat.randg(shape); }); + return mat; + } + if (shape < 1) + shape += 1; + a1 = shape - 1 / 3; + a2 = 1 / Math.sqrt(9 * a1); + do { + do { + x = jStat.randn(); + v = 1 + a2 * x; + } while(v <= 0); + v = v * v * v; + u = jStat._random_fn(); + } while(u > 1 - 0.331 * Math.pow(x, 4) && + Math.log(u) > 0.5 * x*x + a1 * (1 - v + Math.log(v))); + // alpha > 1 + if (shape == oalph) + return a1 * v; + // alpha < 1 + do { + u = jStat._random_fn(); + } while(u === 0); + return Math.pow(u, 1 / oalph) * a1 * v; +}; + + +// making use of static methods on the instance +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jStat.fn[passfunc] = function() { + return jStat( + jStat.map(this, function(value) { return jStat[passfunc](value); })); + } + })(funcs[i]); +})('gammaln gammafn factorial factorialln'.split(' ')); + + +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jStat.fn[passfunc] = function() { + return jStat(jStat[passfunc].apply(null, arguments)); + }; + })(funcs[i]); +})('randn'.split(' ')); + +}(jStat, Math)); +(function(jStat, Math) { + +// generate all distribution instance methods +(function(list) { + for (var i = 0; i < list.length; i++) (function(func) { + // distribution instance method + jStat[func] = function f(a, b, c) { + if (!(this instanceof f)) + return new f(a, b, c); + this._a = a; + this._b = b; + this._c = c; + return this; + }; + // distribution method to be used on a jStat instance + jStat.fn[func] = function(a, b, c) { + var newthis = jStat[func](a, b, c); + newthis.data = this; + return newthis; + }; + // sample instance method + jStat[func].prototype.sample = function(arr) { + var a = this._a; + var b = this._b; + var c = this._c; + if (arr) + return jStat.alter(arr, function() { + return jStat[func].sample(a, b, c); + }); + else + return jStat[func].sample(a, b, c); + }; + // generate the pdf, cdf and inv instance methods + (function(vals) { + for (var i = 0; i < vals.length; i++) (function(fnfunc) { + jStat[func].prototype[fnfunc] = function(x) { + var a = this._a; + var b = this._b; + var c = this._c; + if (!x && x !== 0) + x = this.data; + if (typeof x !== 'number') { + return jStat.fn.map.call(x, function(x) { + return jStat[func][fnfunc](x, a, b, c); + }); + } + return jStat[func][fnfunc](x, a, b, c); + }; + })(vals[i]); + })('pdf cdf inv'.split(' ')); + // generate the mean, median, mode and variance instance methods + (function(vals) { + for (var i = 0; i < vals.length; i++) (function(fnfunc) { + jStat[func].prototype[fnfunc] = function() { + return jStat[func][fnfunc](this._a, this._b, this._c); + }; + })(vals[i]); + })('mean median mode variance'.split(' ')); + })(list[i]); +})(( + 'beta centralF cauchy chisquare exponential gamma invgamma kumaraswamy ' + + 'laplace lognormal noncentralt normal pareto studentt weibull uniform ' + + 'binomial negbin hypgeom poisson triangular tukey arcsine' +).split(' ')); + + + +// extend beta function with static methods +jStat.extend(jStat.beta, { + pdf: function pdf(x, alpha, beta) { + // PDF is zero outside the support + if (x > 1 || x < 0) + return 0; + // PDF is one for the uniform case + if (alpha == 1 && beta == 1) + return 1; + + if (alpha < 512 && beta < 512) { + return (Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1)) / + jStat.betafn(alpha, beta); + } else { + return Math.exp((alpha - 1) * Math.log(x) + + (beta - 1) * Math.log(1 - x) - + jStat.betaln(alpha, beta)); + } + }, + + cdf: function cdf(x, alpha, beta) { + return (x > 1 || x < 0) ? (x > 1) * 1 : jStat.ibeta(x, alpha, beta); + }, + + inv: function inv(x, alpha, beta) { + return jStat.ibetainv(x, alpha, beta); + }, + + mean: function mean(alpha, beta) { + return alpha / (alpha + beta); + }, + + median: function median(alpha, beta) { + return jStat.ibetainv(0.5, alpha, beta); + }, + + mode: function mode(alpha, beta) { + return (alpha - 1 ) / ( alpha + beta - 2); + }, + + // return a random sample + sample: function sample(alpha, beta) { + var u = jStat.randg(alpha); + return u / (u + jStat.randg(beta)); + }, + + variance: function variance(alpha, beta) { + return (alpha * beta) / (Math.pow(alpha + beta, 2) * (alpha + beta + 1)); + } +}); + +// extend F function with static methods +jStat.extend(jStat.centralF, { + // This implementation of the pdf function avoids float overflow + // See the way that R calculates this value: + // https://svn.r-project.org/R/trunk/src/nmath/df.c + pdf: function pdf(x, df1, df2) { + var p, q, f; + + if (x < 0) + return 0; + + if (df1 <= 2) { + if (x === 0 && df1 < 2) { + return Infinity; + } + if (x === 0 && df1 === 2) { + return 1; + } + return (1 / jStat.betafn(df1 / 2, df2 / 2)) * + Math.pow(df1 / df2, df1 / 2) * + Math.pow(x, (df1/2) - 1) * + Math.pow((1 + (df1 / df2) * x), -(df1 + df2) / 2); + } + + p = (df1 * x) / (df2 + x * df1); + q = df2 / (df2 + x * df1); + f = df1 * q / 2.0; + return f * jStat.binomial.pdf((df1 - 2) / 2, (df1 + df2 - 2) / 2, p); + }, + + cdf: function cdf(x, df1, df2) { + if (x < 0) + return 0; + return jStat.ibeta((df1 * x) / (df1 * x + df2), df1 / 2, df2 / 2); + }, + + inv: function inv(x, df1, df2) { + return df2 / (df1 * (1 / jStat.ibetainv(x, df1 / 2, df2 / 2) - 1)); + }, + + mean: function mean(df1, df2) { + return (df2 > 2) ? df2 / (df2 - 2) : undefined; + }, + + mode: function mode(df1, df2) { + return (df1 > 2) ? (df2 * (df1 - 2)) / (df1 * (df2 + 2)) : undefined; + }, + + // return a random sample + sample: function sample(df1, df2) { + var x1 = jStat.randg(df1 / 2) * 2; + var x2 = jStat.randg(df2 / 2) * 2; + return (x1 / df1) / (x2 / df2); + }, + + variance: function variance(df1, df2) { + if (df2 <= 4) + return undefined; + return 2 * df2 * df2 * (df1 + df2 - 2) / + (df1 * (df2 - 2) * (df2 - 2) * (df2 - 4)); + } +}); + + +// extend cauchy function with static methods +jStat.extend(jStat.cauchy, { + pdf: function pdf(x, local, scale) { + if (scale < 0) { return 0; } + + return (scale / (Math.pow(x - local, 2) + Math.pow(scale, 2))) / Math.PI; + }, + + cdf: function cdf(x, local, scale) { + return Math.atan((x - local) / scale) / Math.PI + 0.5; + }, + + inv: function(p, local, scale) { + return local + scale * Math.tan(Math.PI * (p - 0.5)); + }, + + median: function median(local/*, scale*/) { + return local; + }, + + mode: function mode(local/*, scale*/) { + return local; + }, + + sample: function sample(local, scale) { + return jStat.randn() * + Math.sqrt(1 / (2 * jStat.randg(0.5))) * scale + local; + } +}); + + + +// extend chisquare function with static methods +jStat.extend(jStat.chisquare, { + pdf: function pdf(x, dof) { + if (x < 0) + return 0; + return (x === 0 && dof === 2) ? 0.5 : + Math.exp((dof / 2 - 1) * Math.log(x) - x / 2 - (dof / 2) * + Math.log(2) - jStat.gammaln(dof / 2)); + }, + + cdf: function cdf(x, dof) { + if (x < 0) + return 0; + return jStat.lowRegGamma(dof / 2, x / 2); + }, + + inv: function(p, dof) { + return 2 * jStat.gammapinv(p, 0.5 * dof); + }, + + mean : function(dof) { + return dof; + }, + + // TODO: this is an approximation (is there a better way?) + median: function median(dof) { + return dof * Math.pow(1 - (2 / (9 * dof)), 3); + }, + + mode: function mode(dof) { + return (dof - 2 > 0) ? dof - 2 : 0; + }, + + sample: function sample(dof) { + return jStat.randg(dof / 2) * 2; + }, + + variance: function variance(dof) { + return 2 * dof; + } +}); + + + +// extend exponential function with static methods +jStat.extend(jStat.exponential, { + pdf: function pdf(x, rate) { + return x < 0 ? 0 : rate * Math.exp(-rate * x); + }, + + cdf: function cdf(x, rate) { + return x < 0 ? 0 : 1 - Math.exp(-rate * x); + }, + + inv: function(p, rate) { + return -Math.log(1 - p) / rate; + }, + + mean : function(rate) { + return 1 / rate; + }, + + median: function (rate) { + return (1 / rate) * Math.log(2); + }, + + mode: function mode(/*rate*/) { + return 0; + }, + + sample: function sample(rate) { + return -1 / rate * Math.log(jStat._random_fn()); + }, + + variance : function(rate) { + return Math.pow(rate, -2); + } +}); + + + +// extend gamma function with static methods +jStat.extend(jStat.gamma, { + pdf: function pdf(x, shape, scale) { + if (x < 0) + return 0; + return (x === 0 && shape === 1) ? 1 / scale : + Math.exp((shape - 1) * Math.log(x) - x / scale - + jStat.gammaln(shape) - shape * Math.log(scale)); + }, + + cdf: function cdf(x, shape, scale) { + if (x < 0) + return 0; + return jStat.lowRegGamma(shape, x / scale); + }, + + inv: function(p, shape, scale) { + return jStat.gammapinv(p, shape) * scale; + }, + + mean : function(shape, scale) { + return shape * scale; + }, + + mode: function mode(shape, scale) { + if(shape > 1) return (shape - 1) * scale; + return undefined; + }, + + sample: function sample(shape, scale) { + return jStat.randg(shape) * scale; + }, + + variance: function variance(shape, scale) { + return shape * scale * scale; + } +}); + +// extend inverse gamma function with static methods +jStat.extend(jStat.invgamma, { + pdf: function pdf(x, shape, scale) { + if (x <= 0) + return 0; + return Math.exp(-(shape + 1) * Math.log(x) - scale / x - + jStat.gammaln(shape) + shape * Math.log(scale)); + }, + + cdf: function cdf(x, shape, scale) { + if (x <= 0) + return 0; + return 1 - jStat.lowRegGamma(shape, scale / x); + }, + + inv: function(p, shape, scale) { + return scale / jStat.gammapinv(1 - p, shape); + }, + + mean : function(shape, scale) { + return (shape > 1) ? scale / (shape - 1) : undefined; + }, + + mode: function mode(shape, scale) { + return scale / (shape + 1); + }, + + sample: function sample(shape, scale) { + return scale / jStat.randg(shape); + }, + + variance: function variance(shape, scale) { + if (shape <= 2) + return undefined; + return scale * scale / ((shape - 1) * (shape - 1) * (shape - 2)); + } +}); + + +// extend kumaraswamy function with static methods +jStat.extend(jStat.kumaraswamy, { + pdf: function pdf(x, alpha, beta) { + if (x === 0 && alpha === 1) + return beta; + else if (x === 1 && beta === 1) + return alpha; + return Math.exp(Math.log(alpha) + Math.log(beta) + (alpha - 1) * + Math.log(x) + (beta - 1) * + Math.log(1 - Math.pow(x, alpha))); + }, + + cdf: function cdf(x, alpha, beta) { + if (x < 0) + return 0; + else if (x > 1) + return 1; + return (1 - Math.pow(1 - Math.pow(x, alpha), beta)); + }, + + inv: function inv(p, alpha, beta) { + return Math.pow(1 - Math.pow(1 - p, 1 / beta), 1 / alpha); + }, + + mean : function(alpha, beta) { + return (beta * jStat.gammafn(1 + 1 / alpha) * + jStat.gammafn(beta)) / (jStat.gammafn(1 + 1 / alpha + beta)); + }, + + median: function median(alpha, beta) { + return Math.pow(1 - Math.pow(2, -1 / beta), 1 / alpha); + }, + + mode: function mode(alpha, beta) { + if (!(alpha >= 1 && beta >= 1 && (alpha !== 1 && beta !== 1))) + return undefined; + return Math.pow((alpha - 1) / (alpha * beta - 1), 1 / alpha); + }, + + variance: function variance(/*alpha, beta*/) { + throw new Error('variance not yet implemented'); + // TODO: complete this + } +}); + + + +// extend lognormal function with static methods +jStat.extend(jStat.lognormal, { + pdf: function pdf(x, mu, sigma) { + if (x <= 0) + return 0; + return Math.exp(-Math.log(x) - 0.5 * Math.log(2 * Math.PI) - + Math.log(sigma) - Math.pow(Math.log(x) - mu, 2) / + (2 * sigma * sigma)); + }, + + cdf: function cdf(x, mu, sigma) { + if (x < 0) + return 0; + return 0.5 + + (0.5 * jStat.erf((Math.log(x) - mu) / Math.sqrt(2 * sigma * sigma))); + }, + + inv: function(p, mu, sigma) { + return Math.exp(-1.41421356237309505 * sigma * jStat.erfcinv(2 * p) + mu); + }, + + mean: function mean(mu, sigma) { + return Math.exp(mu + sigma * sigma / 2); + }, + + median: function median(mu/*, sigma*/) { + return Math.exp(mu); + }, + + mode: function mode(mu, sigma) { + return Math.exp(mu - sigma * sigma); + }, + + sample: function sample(mu, sigma) { + return Math.exp(jStat.randn() * sigma + mu); + }, + + variance: function variance(mu, sigma) { + return (Math.exp(sigma * sigma) - 1) * Math.exp(2 * mu + sigma * sigma); + } +}); + + + +// extend noncentralt function with static methods +jStat.extend(jStat.noncentralt, { + pdf: function pdf(x, dof, ncp) { + var tol = 1e-14; + if (Math.abs(ncp) < tol) // ncp approx 0; use student-t + return jStat.studentt.pdf(x, dof) + + if (Math.abs(x) < tol) { // different formula for x == 0 + return Math.exp(jStat.gammaln((dof + 1) / 2) - ncp * ncp / 2 - + 0.5 * Math.log(Math.PI * dof) - jStat.gammaln(dof / 2)); + } + + // formula for x != 0 + return dof / x * + (jStat.noncentralt.cdf(x * Math.sqrt(1 + 2 / dof), dof+2, ncp) - + jStat.noncentralt.cdf(x, dof, ncp)); + }, + + cdf: function cdf(x, dof, ncp) { + var tol = 1e-14; + var min_iterations = 200; + + if (Math.abs(ncp) < tol) // ncp approx 0; use student-t + return jStat.studentt.cdf(x, dof); + + // turn negative x into positive and flip result afterwards + var flip = false; + if (x < 0) { + flip = true; + ncp = -ncp; + } + + var prob = jStat.normal.cdf(-ncp, 0, 1); + var value = tol + 1; + // use value at last two steps to determine convergence + var lastvalue = value; + var y = x * x / (x * x + dof); + var j = 0; + var p = Math.exp(-ncp * ncp / 2); + var q = Math.exp(-ncp * ncp / 2 - 0.5 * Math.log(2) - + jStat.gammaln(3 / 2)) * ncp; + while (j < min_iterations || lastvalue > tol || value > tol) { + lastvalue = value; + if (j > 0) { + p *= (ncp * ncp) / (2 * j); + q *= (ncp * ncp) / (2 * (j + 1 / 2)); + } + value = p * jStat.beta.cdf(y, j + 0.5, dof / 2) + + q * jStat.beta.cdf(y, j+1, dof/2); + prob += 0.5 * value; + j++; + } + + return flip ? (1 - prob) : prob; + } +}); + + +// extend normal function with static methods +jStat.extend(jStat.normal, { + pdf: function pdf(x, mean, std) { + return Math.exp(-0.5 * Math.log(2 * Math.PI) - + Math.log(std) - Math.pow(x - mean, 2) / (2 * std * std)); + }, + + cdf: function cdf(x, mean, std) { + return 0.5 * (1 + jStat.erf((x - mean) / Math.sqrt(2 * std * std))); + }, + + inv: function(p, mean, std) { + return -1.41421356237309505 * std * jStat.erfcinv(2 * p) + mean; + }, + + mean : function(mean/*, std*/) { + return mean; + }, + + median: function median(mean/*, std*/) { + return mean; + }, + + mode: function (mean/*, std*/) { + return mean; + }, + + sample: function sample(mean, std) { + return jStat.randn() * std + mean; + }, + + variance : function(mean, std) { + return std * std; + } +}); + + + +// extend pareto function with static methods +jStat.extend(jStat.pareto, { + pdf: function pdf(x, scale, shape) { + if (x < scale) + return 0; + return (shape * Math.pow(scale, shape)) / Math.pow(x, shape + 1); + }, + + cdf: function cdf(x, scale, shape) { + if (x < scale) + return 0; + return 1 - Math.pow(scale / x, shape); + }, + + inv: function inv(p, scale, shape) { + return scale / Math.pow(1 - p, 1 / shape); + }, + + mean: function mean(scale, shape) { + if (shape <= 1) + return undefined; + return (shape * Math.pow(scale, shape)) / (shape - 1); + }, + + median: function median(scale, shape) { + return scale * (shape * Math.SQRT2); + }, + + mode: function mode(scale/*, shape*/) { + return scale; + }, + + variance : function(scale, shape) { + if (shape <= 2) + return undefined; + return (scale*scale * shape) / (Math.pow(shape - 1, 2) * (shape - 2)); + } +}); + + + +// extend studentt function with static methods +jStat.extend(jStat.studentt, { + pdf: function pdf(x, dof) { + dof = dof > 1e100 ? 1e100 : dof; + return (1/(Math.sqrt(dof) * jStat.betafn(0.5, dof/2))) * + Math.pow(1 + ((x * x) / dof), -((dof + 1) / 2)); + }, + + cdf: function cdf(x, dof) { + var dof2 = dof / 2; + return jStat.ibeta((x + Math.sqrt(x * x + dof)) / + (2 * Math.sqrt(x * x + dof)), dof2, dof2); + }, + + inv: function(p, dof) { + var x = jStat.ibetainv(2 * Math.min(p, 1 - p), 0.5 * dof, 0.5); + x = Math.sqrt(dof * (1 - x) / x); + return (p > 0.5) ? x : -x; + }, + + mean: function mean(dof) { + return (dof > 1) ? 0 : undefined; + }, + + median: function median(/*dof*/) { + return 0; + }, + + mode: function mode(/*dof*/) { + return 0; + }, + + sample: function sample(dof) { + return jStat.randn() * Math.sqrt(dof / (2 * jStat.randg(dof / 2))); + }, + + variance: function variance(dof) { + return (dof > 2) ? dof / (dof - 2) : (dof > 1) ? Infinity : undefined; + } +}); + + + +// extend weibull function with static methods +jStat.extend(jStat.weibull, { + pdf: function pdf(x, scale, shape) { + if (x < 0 || scale < 0 || shape < 0) + return 0; + return (shape / scale) * Math.pow((x / scale), (shape - 1)) * + Math.exp(-(Math.pow((x / scale), shape))); + }, + + cdf: function cdf(x, scale, shape) { + return x < 0 ? 0 : 1 - Math.exp(-Math.pow((x / scale), shape)); + }, + + inv: function(p, scale, shape) { + return scale * Math.pow(-Math.log(1 - p), 1 / shape); + }, + + mean : function(scale, shape) { + return scale * jStat.gammafn(1 + 1 / shape); + }, + + median: function median(scale, shape) { + return scale * Math.pow(Math.log(2), 1 / shape); + }, + + mode: function mode(scale, shape) { + if (shape <= 1) + return 0; + return scale * Math.pow((shape - 1) / shape, 1 / shape); + }, + + sample: function sample(scale, shape) { + return scale * Math.pow(-Math.log(jStat._random_fn()), 1 / shape); + }, + + variance: function variance(scale, shape) { + return scale * scale * jStat.gammafn(1 + 2 / shape) - + Math.pow(jStat.weibull.mean(scale, shape), 2); + } +}); + + + +// extend uniform function with static methods +jStat.extend(jStat.uniform, { + pdf: function pdf(x, a, b) { + return (x < a || x > b) ? 0 : 1 / (b - a); + }, + + cdf: function cdf(x, a, b) { + if (x < a) + return 0; + else if (x < b) + return (x - a) / (b - a); + return 1; + }, + + inv: function(p, a, b) { + return a + (p * (b - a)); + }, + + mean: function mean(a, b) { + return 0.5 * (a + b); + }, + + median: function median(a, b) { + return jStat.mean(a, b); + }, + + mode: function mode(/*a, b*/) { + throw new Error('mode is not yet implemented'); + }, + + sample: function sample(a, b) { + return (a / 2 + b / 2) + (b / 2 - a / 2) * (2 * jStat._random_fn() - 1); + }, + + variance: function variance(a, b) { + return Math.pow(b - a, 2) / 12; + } +}); + + +// Got this from http://www.math.ucla.edu/~tom/distributions/binomial.html +function betinc(x, a, b, eps) { + var a0 = 0; + var b0 = 1; + var a1 = 1; + var b1 = 1; + var m9 = 0; + var a2 = 0; + var c9; + + while (Math.abs((a1 - a2) / a1) > eps) { + a2 = a1; + c9 = -(a + m9) * (a + b + m9) * x / (a + 2 * m9) / (a + 2 * m9 + 1); + a0 = a1 + c9 * a0; + b0 = b1 + c9 * b0; + m9 = m9 + 1; + c9 = m9 * (b - m9) * x / (a + 2 * m9 - 1) / (a + 2 * m9); + a1 = a0 + c9 * a1; + b1 = b0 + c9 * b1; + a0 = a0 / b1; + b0 = b0 / b1; + a1 = a1 / b1; + b1 = 1; + } + + return a1 / a; +} + + +// extend uniform function with static methods +jStat.extend(jStat.binomial, { + pdf: function pdf(k, n, p) { + return (p === 0 || p === 1) ? + ((n * p) === k ? 1 : 0) : + jStat.combination(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k); + }, + + cdf: function cdf(x, n, p) { + var betacdf; + var eps = 1e-10; + + if (x < 0) + return 0; + if (x >= n) + return 1; + if (p < 0 || p > 1 || n <= 0) + return NaN; + + x = Math.floor(x); + var z = p; + var a = x + 1; + var b = n - x; + var s = a + b; + var bt = Math.exp(jStat.gammaln(s) - jStat.gammaln(b) - + jStat.gammaln(a) + a * Math.log(z) + b * Math.log(1 - z)); + + if (z < (a + 1) / (s + 2)) + betacdf = bt * betinc(z, a, b, eps); + else + betacdf = 1 - bt * betinc(1 - z, b, a, eps); + + return Math.round((1 - betacdf) * (1 / eps)) / (1 / eps); + } +}); + + + +// extend uniform function with static methods +jStat.extend(jStat.negbin, { + pdf: function pdf(k, r, p) { + if (k !== k >>> 0) + return false; + if (k < 0) + return 0; + return jStat.combination(k + r - 1, r - 1) * + Math.pow(1 - p, k) * Math.pow(p, r); + }, + + cdf: function cdf(x, r, p) { + var sum = 0, + k = 0; + if (x < 0) return 0; + for (; k <= x; k++) { + sum += jStat.negbin.pdf(k, r, p); + } + return sum; + } +}); + + + +// extend uniform function with static methods +jStat.extend(jStat.hypgeom, { + pdf: function pdf(k, N, m, n) { + // Hypergeometric PDF. + + // A simplification of the CDF algorithm below. + + // k = number of successes drawn + // N = population size + // m = number of successes in population + // n = number of items drawn from population + + if(k !== k | 0) { + return false; + } else if(k < 0 || k < m - (N - n)) { + // It's impossible to have this few successes drawn. + return 0; + } else if(k > n || k > m) { + // It's impossible to have this many successes drawn. + return 0; + } else if (m * 2 > N) { + // More than half the population is successes. + + if(n * 2 > N) { + // More than half the population is sampled. + + return jStat.hypgeom.pdf(N - m - n + k, N, N - m, N - n) + } else { + // Half or less of the population is sampled. + + return jStat.hypgeom.pdf(n - k, N, N - m, n); + } + + } else if(n * 2 > N) { + // Half or less is successes. + + return jStat.hypgeom.pdf(m - k, N, m, N - n); + + } else if(m < n) { + // We want to have the number of things sampled to be less than the + // successes available. So swap the definitions of successful and sampled. + return jStat.hypgeom.pdf(k, N, n, m); + } else { + // If we get here, half or less of the population was sampled, half or + // less of it was successes, and we had fewer sampled things than + // successes. Now we can do this complicated iterative algorithm in an + // efficient way. + + // The basic premise of the algorithm is that we partially normalize our + // intermediate product to keep it in a numerically good region, and then + // finish the normalization at the end. + + // This variable holds the scaled probability of the current number of + // successes. + var scaledPDF = 1; + + // This keeps track of how much we have normalized. + var samplesDone = 0; + + for(var i = 0; i < k; i++) { + // For every possible number of successes up to that observed... + + while(scaledPDF > 1 && samplesDone < n) { + // Intermediate result is growing too big. Apply some of the + // normalization to shrink everything. + + scaledPDF *= 1 - (m / (N - samplesDone)); + + // Say we've normalized by this sample already. + samplesDone++; + } + + // Work out the partially-normalized hypergeometric PDF for the next + // number of successes + scaledPDF *= (n - i) * (m - i) / ((i + 1) * (N - m - n + i + 1)); + } + + for(; samplesDone < n; samplesDone++) { + // Apply all the rest of the normalization + scaledPDF *= 1 - (m / (N - samplesDone)); + } + + // Bound answer sanely before returning. + return Math.min(1, Math.max(0, scaledPDF)); + } + }, + + cdf: function cdf(x, N, m, n) { + // Hypergeometric CDF. + + // This algorithm is due to Prof. Thomas S. Ferguson, , + // and comes from his hypergeometric test calculator at + // . + + // x = number of successes drawn + // N = population size + // m = number of successes in population + // n = number of items drawn from population + + if(x < 0 || x < m - (N - n)) { + // It's impossible to have this few successes drawn or fewer. + return 0; + } else if(x >= n || x >= m) { + // We will always have this many successes or fewer. + return 1; + } else if (m * 2 > N) { + // More than half the population is successes. + + if(n * 2 > N) { + // More than half the population is sampled. + + return jStat.hypgeom.cdf(N - m - n + x, N, N - m, N - n) + } else { + // Half or less of the population is sampled. + + return 1 - jStat.hypgeom.cdf(n - x - 1, N, N - m, n); + } + + } else if(n * 2 > N) { + // Half or less is successes. + + return 1 - jStat.hypgeom.cdf(m - x - 1, N, m, N - n); + + } else if(m < n) { + // We want to have the number of things sampled to be less than the + // successes available. So swap the definitions of successful and sampled. + return jStat.hypgeom.cdf(x, N, n, m); + } else { + // If we get here, half or less of the population was sampled, half or + // less of it was successes, and we had fewer sampled things than + // successes. Now we can do this complicated iterative algorithm in an + // efficient way. + + // The basic premise of the algorithm is that we partially normalize our + // intermediate sum to keep it in a numerically good region, and then + // finish the normalization at the end. + + // Holds the intermediate, scaled total CDF. + var scaledCDF = 1; + + // This variable holds the scaled probability of the current number of + // successes. + var scaledPDF = 1; + + // This keeps track of how much we have normalized. + var samplesDone = 0; + + for(var i = 0; i < x; i++) { + // For every possible number of successes up to that observed... + + while(scaledCDF > 1 && samplesDone < n) { + // Intermediate result is growing too big. Apply some of the + // normalization to shrink everything. + + var factor = 1 - (m / (N - samplesDone)); + + scaledPDF *= factor; + scaledCDF *= factor; + + // Say we've normalized by this sample already. + samplesDone++; + } + + // Work out the partially-normalized hypergeometric PDF for the next + // number of successes + scaledPDF *= (n - i) * (m - i) / ((i + 1) * (N - m - n + i + 1)); + + // Add to the CDF answer. + scaledCDF += scaledPDF; + } + + for(; samplesDone < n; samplesDone++) { + // Apply all the rest of the normalization + scaledCDF *= 1 - (m / (N - samplesDone)); + } + + // Bound answer sanely before returning. + return Math.min(1, Math.max(0, scaledCDF)); + } + } +}); + + + +// extend uniform function with static methods +jStat.extend(jStat.poisson, { + pdf: function pdf(k, l) { + if (l < 0 || (k % 1) !== 0 || k < 0) { + return 0; + } + + return Math.pow(l, k) * Math.exp(-l) / jStat.factorial(k); + }, + + cdf: function cdf(x, l) { + var sumarr = [], + k = 0; + if (x < 0) return 0; + for (; k <= x; k++) { + sumarr.push(jStat.poisson.pdf(k, l)); + } + return jStat.sum(sumarr); + }, + + mean : function(l) { + return l; + }, + + variance : function(l) { + return l; + }, + + sampleSmall: function sampleSmall(l) { + var p = 1, k = 0, L = Math.exp(-l); + do { + k++; + p *= jStat._random_fn(); + } while (p > L); + return k - 1; + }, + + sampleLarge: function sampleLarge(l) { + var lam = l; + var k; + var U, V, slam, loglam, a, b, invalpha, vr, us; + + slam = Math.sqrt(lam); + loglam = Math.log(lam); + b = 0.931 + 2.53 * slam; + a = -0.059 + 0.02483 * b; + invalpha = 1.1239 + 1.1328 / (b - 3.4); + vr = 0.9277 - 3.6224 / (b - 2); + + while (1) { + U = Math.random() - 0.5; + V = Math.random(); + us = 0.5 - Math.abs(U); + k = Math.floor((2 * a / us + b) * U + lam + 0.43); + if ((us >= 0.07) && (V <= vr)) { + return k; + } + if ((k < 0) || ((us < 0.013) && (V > us))) { + continue; + } + /* log(V) == log(0.0) ok here */ + /* if U==0.0 so that us==0.0, log is ok since always returns */ + if ((Math.log(V) + Math.log(invalpha) - Math.log(a / (us * us) + b)) <= (-lam + k * loglam - jStat.loggam(k + 1))) { + return k; + } + } + }, + + sample: function sample(l) { + if (l < 10) + return this.sampleSmall(l); + else + return this.sampleLarge(l); + } +}); + +// extend triangular function with static methods +jStat.extend(jStat.triangular, { + pdf: function pdf(x, a, b, c) { + if (b <= a || c < a || c > b) { + return NaN; + } else { + if (x < a || x > b) { + return 0; + } else if (x < c) { + return (2 * (x - a)) / ((b - a) * (c - a)); + } else if (x === c) { + return (2 / (b - a)); + } else { // x > c + return (2 * (b - x)) / ((b - a) * (b - c)); + } + } + }, + + cdf: function cdf(x, a, b, c) { + if (b <= a || c < a || c > b) + return NaN; + if (x <= a) + return 0; + else if (x >= b) + return 1; + if (x <= c) + return Math.pow(x - a, 2) / ((b - a) * (c - a)); + else // x > c + return 1 - Math.pow(b - x, 2) / ((b - a) * (b - c)); + }, + + inv: function inv(p, a, b, c) { + if (b <= a || c < a || c > b) { + return NaN; + } else { + if (p <= ((c - a) / (b - a))) { + return a + (b - a) * Math.sqrt(p * ((c - a) / (b - a))); + } else { // p > ((c - a) / (b - a)) + return a + (b - a) * (1 - Math.sqrt((1 - p) * (1 - ((c - a) / (b - a))))); + } + } + }, + + mean: function mean(a, b, c) { + return (a + b + c) / 3; + }, + + median: function median(a, b, c) { + if (c <= (a + b) / 2) { + return b - Math.sqrt((b - a) * (b - c)) / Math.sqrt(2); + } else if (c > (a + b) / 2) { + return a + Math.sqrt((b - a) * (c - a)) / Math.sqrt(2); + } + }, + + mode: function mode(a, b, c) { + return c; + }, + + sample: function sample(a, b, c) { + var u = jStat._random_fn(); + if (u < ((c - a) / (b - a))) + return a + Math.sqrt(u * (b - a) * (c - a)) + return b - Math.sqrt((1 - u) * (b - a) * (b - c)); + }, + + variance: function variance(a, b, c) { + return (a * a + b * b + c * c - a * b - a * c - b * c) / 18; + } +}); + + +// extend arcsine function with static methods +jStat.extend(jStat.arcsine, { + pdf: function pdf(x, a, b) { + if (b <= a) return NaN; + + return (x <= a || x >= b) ? 0 : + (2 / Math.PI) * + Math.pow(Math.pow(b - a, 2) - + Math.pow(2 * x - a - b, 2), -0.5); + }, + + cdf: function cdf(x, a, b) { + if (x < a) + return 0; + else if (x < b) + return (2 / Math.PI) * Math.asin(Math.sqrt((x - a)/(b - a))); + return 1; + }, + + inv: function(p, a, b) { + return a + (0.5 - 0.5 * Math.cos(Math.PI * p)) * (b - a); + }, + + mean: function mean(a, b) { + if (b <= a) return NaN; + return (a + b) / 2; + }, + + median: function median(a, b) { + if (b <= a) return NaN; + return (a + b) / 2; + }, + + mode: function mode(/*a, b*/) { + throw new Error('mode is not yet implemented'); + }, + + sample: function sample(a, b) { + return ((a + b) / 2) + ((b - a) / 2) * + Math.sin(2 * Math.PI * jStat.uniform.sample(0, 1)); + }, + + variance: function variance(a, b) { + if (b <= a) return NaN; + return Math.pow(b - a, 2) / 8; + } +}); + + +function laplaceSign(x) { return x / Math.abs(x); } + +jStat.extend(jStat.laplace, { + pdf: function pdf(x, mu, b) { + return (b <= 0) ? 0 : (Math.exp(-Math.abs(x - mu) / b)) / (2 * b); + }, + + cdf: function cdf(x, mu, b) { + if (b <= 0) { return 0; } + + if(x < mu) { + return 0.5 * Math.exp((x - mu) / b); + } else { + return 1 - 0.5 * Math.exp(- (x - mu) / b); + } + }, + + mean: function(mu/*, b*/) { + return mu; + }, + + median: function(mu/*, b*/) { + return mu; + }, + + mode: function(mu/*, b*/) { + return mu; + }, + + variance: function(mu, b) { + return 2 * b * b; + }, + + sample: function sample(mu, b) { + var u = jStat._random_fn() - 0.5; + + return mu - (b * laplaceSign(u) * Math.log(1 - (2 * Math.abs(u)))); + } +}); + +function tukeyWprob(w, rr, cc) { + var nleg = 12; + var ihalf = 6; + + var C1 = -30; + var C2 = -50; + var C3 = 60; + var bb = 8; + var wlar = 3; + var wincr1 = 2; + var wincr2 = 3; + var xleg = [ + 0.981560634246719250690549090149, + 0.904117256370474856678465866119, + 0.769902674194304687036893833213, + 0.587317954286617447296702418941, + 0.367831498998180193752691536644, + 0.125233408511468915472441369464 + ]; + var aleg = [ + 0.047175336386511827194615961485, + 0.106939325995318430960254718194, + 0.160078328543346226334652529543, + 0.203167426723065921749064455810, + 0.233492536538354808760849898925, + 0.249147045813402785000562436043 + ]; + + var qsqz = w * 0.5; + + // if w >= 16 then the integral lower bound (occurs for c=20) + // is 0.99999999999995 so return a value of 1. + + if (qsqz >= bb) + return 1.0; + + // find (f(w/2) - 1) ^ cc + // (first term in integral of hartley's form). + + var pr_w = 2 * jStat.normal.cdf(qsqz, 0, 1, 1, 0) - 1; // erf(qsqz / M_SQRT2) + // if pr_w ^ cc < 2e-22 then set pr_w = 0 + if (pr_w >= Math.exp(C2 / cc)) + pr_w = Math.pow(pr_w, cc); + else + pr_w = 0.0; + + // if w is large then the second component of the + // integral is small, so fewer intervals are needed. + + var wincr; + if (w > wlar) + wincr = wincr1; + else + wincr = wincr2; + + // find the integral of second term of hartley's form + // for the integral of the range for equal-length + // intervals using legendre quadrature. limits of + // integration are from (w/2, 8). two or three + // equal-length intervals are used. + + // blb and bub are lower and upper limits of integration. + + var blb = qsqz; + var binc = (bb - qsqz) / wincr; + var bub = blb + binc; + var einsum = 0.0; + + // integrate over each interval + + var cc1 = cc - 1.0; + for (var wi = 1; wi <= wincr; wi++) { + var elsum = 0.0; + var a = 0.5 * (bub + blb); + + // legendre quadrature with order = nleg + + var b = 0.5 * (bub - blb); + + for (var jj = 1; jj <= nleg; jj++) { + var j, xx; + if (ihalf < jj) { + j = (nleg - jj) + 1; + xx = xleg[j-1]; + } else { + j = jj; + xx = -xleg[j-1]; + } + var c = b * xx; + var ac = a + c; + + // if exp(-qexpo/2) < 9e-14, + // then doesn't contribute to integral + + var qexpo = ac * ac; + if (qexpo > C3) + break; + + var pplus = 2 * jStat.normal.cdf(ac, 0, 1, 1, 0); + var pminus= 2 * jStat.normal.cdf(ac, w, 1, 1, 0); + + // if rinsum ^ (cc-1) < 9e-14, + // then doesn't contribute to integral + + var rinsum = (pplus * 0.5) - (pminus * 0.5); + if (rinsum >= Math.exp(C1 / cc1)) { + rinsum = (aleg[j-1] * Math.exp(-(0.5 * qexpo))) * Math.pow(rinsum, cc1); + elsum += rinsum; + } + } + elsum *= (((2.0 * b) * cc) / Math.sqrt(2 * Math.PI)); + einsum += elsum; + blb = bub; + bub += binc; + } + + // if pr_w ^ rr < 9e-14, then return 0 + pr_w += einsum; + if (pr_w <= Math.exp(C1 / rr)) + return 0; + + pr_w = Math.pow(pr_w, rr); + if (pr_w >= 1) // 1 was iMax was eps + return 1; + return pr_w; +} + +function tukeyQinv(p, c, v) { + var p0 = 0.322232421088; + var q0 = 0.993484626060e-01; + var p1 = -1.0; + var q1 = 0.588581570495; + var p2 = -0.342242088547; + var q2 = 0.531103462366; + var p3 = -0.204231210125; + var q3 = 0.103537752850; + var p4 = -0.453642210148e-04; + var q4 = 0.38560700634e-02; + var c1 = 0.8832; + var c2 = 0.2368; + var c3 = 1.214; + var c4 = 1.208; + var c5 = 1.4142; + var vmax = 120.0; + + var ps = 0.5 - 0.5 * p; + var yi = Math.sqrt(Math.log(1.0 / (ps * ps))); + var t = yi + (((( yi * p4 + p3) * yi + p2) * yi + p1) * yi + p0) + / (((( yi * q4 + q3) * yi + q2) * yi + q1) * yi + q0); + if (v < vmax) t += (t * t * t + t) / v / 4.0; + var q = c1 - c2 * t; + if (v < vmax) q += -c3 / v + c4 * t / v; + return t * (q * Math.log(c - 1.0) + c5); +} + +jStat.extend(jStat.tukey, { + cdf: function cdf(q, nmeans, df) { + // Identical implementation as the R ptukey() function as of commit 68947 + var rr = 1; + var cc = nmeans; + + var nlegq = 16; + var ihalfq = 8; + + var eps1 = -30.0; + var eps2 = 1.0e-14; + var dhaf = 100.0; + var dquar = 800.0; + var deigh = 5000.0; + var dlarg = 25000.0; + var ulen1 = 1.0; + var ulen2 = 0.5; + var ulen3 = 0.25; + var ulen4 = 0.125; + var xlegq = [ + 0.989400934991649932596154173450, + 0.944575023073232576077988415535, + 0.865631202387831743880467897712, + 0.755404408355003033895101194847, + 0.617876244402643748446671764049, + 0.458016777657227386342419442984, + 0.281603550779258913230460501460, + 0.950125098376374401853193354250e-1 + ]; + var alegq = [ + 0.271524594117540948517805724560e-1, + 0.622535239386478928628438369944e-1, + 0.951585116824927848099251076022e-1, + 0.124628971255533872052476282192, + 0.149595988816576732081501730547, + 0.169156519395002538189312079030, + 0.182603415044923588866763667969, + 0.189450610455068496285396723208 + ]; + + if (q <= 0) + return 0; + + // df must be > 1 + // there must be at least two values + + if (df < 2 || rr < 1 || cc < 2) return NaN; + + if (!Number.isFinite(q)) + return 1; + + if (df > dlarg) + return tukeyWprob(q, rr, cc); + + // calculate leading constant + + var f2 = df * 0.5; + var f2lf = ((f2 * Math.log(df)) - (df * Math.log(2))) - jStat.gammaln(f2); + var f21 = f2 - 1.0; + + // integral is divided into unit, half-unit, quarter-unit, or + // eighth-unit length intervals depending on the value of the + // degrees of freedom. + + var ff4 = df * 0.25; + var ulen; + if (df <= dhaf) ulen = ulen1; + else if (df <= dquar) ulen = ulen2; + else if (df <= deigh) ulen = ulen3; + else ulen = ulen4; + + f2lf += Math.log(ulen); + + // integrate over each subinterval + + var ans = 0.0; + + for (var i = 1; i <= 50; i++) { + var otsum = 0.0; + + // legendre quadrature with order = nlegq + // nodes (stored in xlegq) are symmetric around zero. + + var twa1 = (2 * i - 1) * ulen; + + for (var jj = 1; jj <= nlegq; jj++) { + var j, t1; + if (ihalfq < jj) { + j = jj - ihalfq - 1; + t1 = (f2lf + (f21 * Math.log(twa1 + (xlegq[j] * ulen)))) + - (((xlegq[j] * ulen) + twa1) * ff4); + } else { + j = jj - 1; + t1 = (f2lf + (f21 * Math.log(twa1 - (xlegq[j] * ulen)))) + + (((xlegq[j] * ulen) - twa1) * ff4); + } + + // if exp(t1) < 9e-14, then doesn't contribute to integral + var qsqz; + if (t1 >= eps1) { + if (ihalfq < jj) { + qsqz = q * Math.sqrt(((xlegq[j] * ulen) + twa1) * 0.5); + } else { + qsqz = q * Math.sqrt(((-(xlegq[j] * ulen)) + twa1) * 0.5); + } + + // call wprob to find integral of range portion + + var wprb = tukeyWprob(qsqz, rr, cc); + var rotsum = (wprb * alegq[j]) * Math.exp(t1); + otsum += rotsum; + } + // end legendre integral for interval i + // L200: + } + + // if integral for interval i < 1e-14, then stop. + // However, in order to avoid small area under left tail, + // at least 1 / ulen intervals are calculated. + if (i * ulen >= 1.0 && otsum <= eps2) + break; + + // end of interval i + // L330: + + ans += otsum; + } + + if (otsum > eps2) { // not converged + throw new Error('tukey.cdf failed to converge'); + } + if (ans > 1) + ans = 1; + return ans; + }, + + inv: function(p, nmeans, df) { + // Identical implementation as the R qtukey() function as of commit 68947 + var rr = 1; + var cc = nmeans; + + var eps = 0.0001; + var maxiter = 50; + + // df must be > 1 ; there must be at least two values + if (df < 2 || rr < 1 || cc < 2) return NaN; + + if (p < 0 || p > 1) return NaN; + if (p === 0) return 0; + if (p === 1) return Infinity; + + // Initial value + + var x0 = tukeyQinv(p, cc, df); + + // Find prob(value < x0) + + var valx0 = jStat.tukey.cdf(x0, nmeans, df) - p; + + // Find the second iterate and prob(value < x1). + // If the first iterate has probability value + // exceeding p then second iterate is 1 less than + // first iterate; otherwise it is 1 greater. + + var x1; + if (valx0 > 0.0) + x1 = Math.max(0.0, x0 - 1.0); + else + x1 = x0 + 1.0; + var valx1 = jStat.tukey.cdf(x1, nmeans, df) - p; + + // Find new iterate + + var ans; + for(var iter = 1; iter < maxiter; iter++) { + ans = x1 - ((valx1 * (x1 - x0)) / (valx1 - valx0)); + valx0 = valx1; + + // New iterate must be >= 0 + + x0 = x1; + if (ans < 0.0) { + ans = 0.0; + valx1 = -p; + } + // Find prob(value < new iterate) + + valx1 = jStat.tukey.cdf(ans, nmeans, df) - p; + x1 = ans; + + // If the difference between two successive + // iterates is less than eps, stop + + var xabs = Math.abs(x1 - x0); + if (xabs < eps) + return ans; + } + + throw new Error('tukey.inv failed to converge'); + } +}); + +}(jStat, Math)); +/* Provides functions for the solution of linear system of equations, integration, extrapolation, + * interpolation, eigenvalue problems, differential equations and PCA analysis. */ + +(function(jStat, Math) { + +var push = Array.prototype.push; +var isArray = jStat.utils.isArray; + +function isUsable(arg) { + return isArray(arg) || arg instanceof jStat; +} + +jStat.extend({ + + // add a vector/matrix to a vector/matrix or scalar + add: function add(arr, arg) { + // check if arg is a vector or scalar + if (isUsable(arg)) { + if (!isUsable(arg[0])) arg = [ arg ]; + return jStat.map(arr, function(value, row, col) { + return value + arg[row][col]; + }); + } + return jStat.map(arr, function(value) { return value + arg; }); + }, + + // subtract a vector or scalar from the vector + subtract: function subtract(arr, arg) { + // check if arg is a vector or scalar + if (isUsable(arg)) { + if (!isUsable(arg[0])) arg = [ arg ]; + return jStat.map(arr, function(value, row, col) { + return value - arg[row][col] || 0; + }); + } + return jStat.map(arr, function(value) { return value - arg; }); + }, + + // matrix division + divide: function divide(arr, arg) { + if (isUsable(arg)) { + if (!isUsable(arg[0])) arg = [ arg ]; + return jStat.multiply(arr, jStat.inv(arg)); + } + return jStat.map(arr, function(value) { return value / arg; }); + }, + + // matrix multiplication + multiply: function multiply(arr, arg) { + var row, col, nrescols, sum, nrow, ncol, res, rescols; + // eg: arr = 2 arg = 3 -> 6 for res[0][0] statement closure + if (arr.length === undefined && arg.length === undefined) { + return arr * arg; + } + nrow = arr.length, + ncol = arr[0].length, + res = jStat.zeros(nrow, nrescols = (isUsable(arg)) ? arg[0].length : ncol), + rescols = 0; + if (isUsable(arg)) { + for (; rescols < nrescols; rescols++) { + for (row = 0; row < nrow; row++) { + sum = 0; + for (col = 0; col < ncol; col++) + sum += arr[row][col] * arg[col][rescols]; + res[row][rescols] = sum; + } + } + return (nrow === 1 && rescols === 1) ? res[0][0] : res; + } + return jStat.map(arr, function(value) { return value * arg; }); + }, + + // outer([1,2,3],[4,5,6]) + // === + // [[1],[2],[3]] times [[4,5,6]] + // -> + // [[4,5,6],[8,10,12],[12,15,18]] + outer:function outer(A, B) { + return jStat.multiply(A.map(function(t){ return [t] }), [B]); + }, + + + // Returns the dot product of two matricies + dot: function dot(arr, arg) { + if (!isUsable(arr[0])) arr = [ arr ]; + if (!isUsable(arg[0])) arg = [ arg ]; + // convert column to row vector + var left = (arr[0].length === 1 && arr.length !== 1) ? jStat.transpose(arr) : arr, + right = (arg[0].length === 1 && arg.length !== 1) ? jStat.transpose(arg) : arg, + res = [], + row = 0, + nrow = left.length, + ncol = left[0].length, + sum, col; + for (; row < nrow; row++) { + res[row] = []; + sum = 0; + for (col = 0; col < ncol; col++) + sum += left[row][col] * right[row][col]; + res[row] = sum; + } + return (res.length === 1) ? res[0] : res; + }, + + // raise every element by a scalar + pow: function pow(arr, arg) { + return jStat.map(arr, function(value) { return Math.pow(value, arg); }); + }, + + // exponentiate every element + exp: function exp(arr) { + return jStat.map(arr, function(value) { return Math.exp(value); }); + }, + + // generate the natural log of every element + log: function exp(arr) { + return jStat.map(arr, function(value) { return Math.log(value); }); + }, + + // generate the absolute values of the vector + abs: function abs(arr) { + return jStat.map(arr, function(value) { return Math.abs(value); }); + }, + + // computes the p-norm of the vector + // In the case that a matrix is passed, uses the first row as the vector + norm: function norm(arr, p) { + var nnorm = 0, + i = 0; + // check the p-value of the norm, and set for most common case + if (isNaN(p)) p = 2; + // check if multi-dimensional array, and make vector correction + if (isUsable(arr[0])) arr = arr[0]; + // vector norm + for (; i < arr.length; i++) { + nnorm += Math.pow(Math.abs(arr[i]), p); + } + return Math.pow(nnorm, 1 / p); + }, + + // computes the angle between two vectors in rads + // In case a matrix is passed, this uses the first row as the vector + angle: function angle(arr, arg) { + return Math.acos(jStat.dot(arr, arg) / (jStat.norm(arr) * jStat.norm(arg))); + }, + + // augment one matrix by another + // Note: this function returns a matrix, not a jStat object + aug: function aug(a, b) { + var newarr = []; + var i; + for (i = 0; i < a.length; i++) { + newarr.push(a[i].slice()); + } + for (i = 0; i < newarr.length; i++) { + push.apply(newarr[i], b[i]); + } + return newarr; + }, + + // The inv() function calculates the inverse of a matrix + // Create the inverse by augmenting the matrix by the identity matrix of the + // appropriate size, and then use G-J elimination on the augmented matrix. + inv: function inv(a) { + var rows = a.length; + var cols = a[0].length; + var b = jStat.identity(rows, cols); + var c = jStat.gauss_jordan(a, b); + var result = []; + var i = 0; + var j; + + //We need to copy the inverse portion to a new matrix to rid G-J artifacts + for (; i < rows; i++) { + result[i] = []; + for (j = cols; j < c[0].length; j++) + result[i][j - cols] = c[i][j]; + } + return result; + }, + + // calculate the determinant of a matrix + det: function det(a) { + if (a.length === 2) { + return a[0][0] * a[1][1] - a[0][1] * a[1][0]; + } + + var determinant = 0; + for (var i = 0; i < a.length; i++) { + // build a sub matrix without column `i` + var submatrix = []; + for (var row = 1; row < a.length; row++) { + submatrix[row - 1] = []; + for (var col = 0; col < a.length; col++) { + if (col < i) { + submatrix[row - 1][col] = a[row][col]; + } else if (col > i) { + submatrix[row - 1][col - 1] = a[row][col]; + } + } + } + + // alternate between + and - between determinants + var sign = i % 2 ? -1 : 1; + determinant += det(submatrix) * a[0][i] * sign; + } + + return determinant + }, + + gauss_elimination: function gauss_elimination(a, b) { + var i = 0, + j = 0, + n = a.length, + m = a[0].length, + factor = 1, + sum = 0, + x = [], + maug, pivot, temp, k; + a = jStat.aug(a, b); + maug = a[0].length; + for(i = 0; i < n; i++) { + pivot = a[i][i]; + j = i; + for (k = i + 1; k < m; k++) { + if (pivot < Math.abs(a[k][i])) { + pivot = a[k][i]; + j = k; + } + } + if (j != i) { + for(k = 0; k < maug; k++) { + temp = a[i][k]; + a[i][k] = a[j][k]; + a[j][k] = temp; + } + } + for (j = i + 1; j < n; j++) { + factor = a[j][i] / a[i][i]; + for(k = i; k < maug; k++) { + a[j][k] = a[j][k] - factor * a[i][k]; + } + } + } + for (i = n - 1; i >= 0; i--) { + sum = 0; + for (j = i + 1; j<= n - 1; j++) { + sum = sum + x[j] * a[i][j]; + } + x[i] =(a[i][maug - 1] - sum) / a[i][i]; + } + return x; + }, + + gauss_jordan: function gauss_jordan(a, b) { + var m = jStat.aug(a, b); + var h = m.length; + var w = m[0].length; + var c = 0; + var x, y, y2; + // find max pivot + for (y = 0; y < h; y++) { + var maxrow = y; + for (y2 = y+1; y2 < h; y2++) { + if (Math.abs(m[y2][y]) > Math.abs(m[maxrow][y])) + maxrow = y2; + } + var tmp = m[y]; + m[y] = m[maxrow]; + m[maxrow] = tmp + for (y2 = y+1; y2 < h; y2++) { + c = m[y2][y] / m[y][y]; + for (x = y; x < w; x++) { + m[y2][x] -= m[y][x] * c; + } + } + } + // backsubstitute + for (y = h-1; y >= 0; y--) { + c = m[y][y]; + for (y2 = 0; y2 < y; y2++) { + for (x = w-1; x > y-1; x--) { + m[y2][x] -= m[y][x] * m[y2][y] / c; + } + } + m[y][y] /= c; + for (x = h; x < w; x++) { + m[y][x] /= c; + } + } + return m; + }, + + // solve equation + // Ax=b + // A is upper triangular matrix + // A=[[1,2,3],[0,4,5],[0,6,7]] + // b=[1,2,3] + // triaUpSolve(A,b) // -> [2.666,0.1666,1.666] + // if you use matrix style + // A=[[1,2,3],[0,4,5],[0,6,7]] + // b=[[1],[2],[3]] + // will return [[2.666],[0.1666],[1.666]] + triaUpSolve: function triaUpSolve(A, b) { + var size = A[0].length; + var x = jStat.zeros(1, size)[0]; + var parts; + var matrix_mode = false; + + if (b[0].length != undefined) { + b = b.map(function(i){ return i[0] }); + matrix_mode = true; + } + + jStat.arange(size - 1, -1, -1).forEach(function(i) { + parts = jStat.arange(i + 1, size).map(function(j) { + return x[j] * A[i][j]; + }); + x[i] = (b[i] - jStat.sum(parts)) / A[i][i]; + }); + + if (matrix_mode) + return x.map(function(i){ return [i] }); + return x; + }, + + triaLowSolve: function triaLowSolve(A, b) { + // like to triaUpSolve but A is lower triangular matrix + var size = A[0].length; + var x = jStat.zeros(1, size)[0]; + var parts; + + var matrix_mode=false; + if (b[0].length != undefined) { + b = b.map(function(i){ return i[0] }); + matrix_mode = true; + } + + jStat.arange(size).forEach(function(i) { + parts = jStat.arange(i).map(function(j) { + return A[i][j] * x[j]; + }); + x[i] = (b[i] - jStat.sum(parts)) / A[i][i]; + }) + + if (matrix_mode) + return x.map(function(i){ return [i] }); + return x; + }, + + + // A -> [L,U] + // A=LU + // L is lower triangular matrix + // U is upper triangular matrix + lu: function lu(A) { + var size = A.length; + //var L=jStat.diagonal(jStat.ones(1,size)[0]); + var L = jStat.identity(size); + var R = jStat.zeros(A.length, A[0].length); + var parts; + jStat.arange(size).forEach(function(t) { + R[0][t] = A[0][t]; + }); + jStat.arange(1, size).forEach(function(l) { + jStat.arange(l).forEach(function(i) { + parts = jStat.arange(i).map(function(jj) { + return L[l][jj] * R[jj][i]; + }); + L[l][i] = (A[l][i] - jStat.sum(parts)) / R[i][i]; + }); + jStat.arange(l, size).forEach(function(j) { + parts = jStat.arange(l).map(function(jj) { + return L[l][jj] * R[jj][j]; + }); + R[l][j] = A[parts.length][j] - jStat.sum(parts); + }); + }); + return [L, R]; + }, + + // A -> T + // A=TT' + // T is lower triangular matrix + cholesky: function cholesky(A) { + var size = A.length; + var T = jStat.zeros(A.length, A[0].length); + var parts; + jStat.arange(size).forEach(function(i) { + parts = jStat.arange(i).map(function(t) { + return Math.pow(T[i][t],2); + }); + T[i][i] = Math.sqrt(A[i][i] - jStat.sum(parts)); + jStat.arange(i + 1, size).forEach(function(j) { + parts = jStat.arange(i).map(function(t) { + return T[i][t] * T[j][t]; + }); + T[j][i] = (A[i][j] - jStat.sum(parts)) / T[i][i]; + }); + }); + return T; + }, + + + gauss_jacobi: function gauss_jacobi(a, b, x, r) { + var i = 0; + var j = 0; + var n = a.length; + var l = []; + var u = []; + var d = []; + var xv, c, h, xk; + for (; i < n; i++) { + l[i] = []; + u[i] = []; + d[i] = []; + for (j = 0; j < n; j++) { + if (i > j) { + l[i][j] = a[i][j]; + u[i][j] = d[i][j] = 0; + } else if (i < j) { + u[i][j] = a[i][j]; + l[i][j] = d[i][j] = 0; + } else { + d[i][j] = a[i][j]; + l[i][j] = u[i][j] = 0; + } + } + } + h = jStat.multiply(jStat.multiply(jStat.inv(d), jStat.add(l, u)), -1); + c = jStat.multiply(jStat.inv(d), b); + xv = x; + xk = jStat.add(jStat.multiply(h, x), c); + i = 2; + while (Math.abs(jStat.norm(jStat.subtract(xk,xv))) > r) { + xv = xk; + xk = jStat.add(jStat.multiply(h, xv), c); + i++; + } + return xk; + }, + + gauss_seidel: function gauss_seidel(a, b, x, r) { + var i = 0; + var n = a.length; + var l = []; + var u = []; + var d = []; + var j, xv, c, h, xk; + for (; i < n; i++) { + l[i] = []; + u[i] = []; + d[i] = []; + for (j = 0; j < n; j++) { + if (i > j) { + l[i][j] = a[i][j]; + u[i][j] = d[i][j] = 0; + } else if (i < j) { + u[i][j] = a[i][j]; + l[i][j] = d[i][j] = 0; + } else { + d[i][j] = a[i][j]; + l[i][j] = u[i][j] = 0; + } + } + } + h = jStat.multiply(jStat.multiply(jStat.inv(jStat.add(d, l)), u), -1); + c = jStat.multiply(jStat.inv(jStat.add(d, l)), b); + xv = x; + xk = jStat.add(jStat.multiply(h, x), c); + i = 2; + while (Math.abs(jStat.norm(jStat.subtract(xk, xv))) > r) { + xv = xk; + xk = jStat.add(jStat.multiply(h, xv), c); + i = i + 1; + } + return xk; + }, + + SOR: function SOR(a, b, x, r, w) { + var i = 0; + var n = a.length; + var l = []; + var u = []; + var d = []; + var j, xv, c, h, xk; + for (; i < n; i++) { + l[i] = []; + u[i] = []; + d[i] = []; + for (j = 0; j < n; j++) { + if (i > j) { + l[i][j] = a[i][j]; + u[i][j] = d[i][j] = 0; + } else if (i < j) { + u[i][j] = a[i][j]; + l[i][j] = d[i][j] = 0; + } else { + d[i][j] = a[i][j]; + l[i][j] = u[i][j] = 0; + } + } + } + h = jStat.multiply(jStat.inv(jStat.add(d, jStat.multiply(l, w))), + jStat.subtract(jStat.multiply(d, 1 - w), + jStat.multiply(u, w))); + c = jStat.multiply(jStat.multiply(jStat.inv(jStat.add(d, + jStat.multiply(l, w))), b), w); + xv = x; + xk = jStat.add(jStat.multiply(h, x), c); + i = 2; + while (Math.abs(jStat.norm(jStat.subtract(xk, xv))) > r) { + xv = xk; + xk = jStat.add(jStat.multiply(h, xv), c); + i++; + } + return xk; + }, + + householder: function householder(a) { + var m = a.length; + var n = a[0].length; + var i = 0; + var w = []; + var p = []; + var alpha, r, k, j, factor; + for (; i < m - 1; i++) { + alpha = 0; + for (j = i + 1; j < n; j++) + alpha += (a[j][i] * a[j][i]); + factor = (a[i + 1][i] > 0) ? -1 : 1; + alpha = factor * Math.sqrt(alpha); + r = Math.sqrt((((alpha * alpha) - a[i + 1][i] * alpha) / 2)); + w = jStat.zeros(m, 1); + w[i + 1][0] = (a[i + 1][i] - alpha) / (2 * r); + for (k = i + 2; k < m; k++) w[k][0] = a[k][i] / (2 * r); + p = jStat.subtract(jStat.identity(m, n), + jStat.multiply(jStat.multiply(w, jStat.transpose(w)), 2)); + a = jStat.multiply(p, jStat.multiply(a, p)); + } + return a; + }, + + // A -> [Q,R] + // Q is orthogonal matrix + // R is upper triangular + QR: (function() { + // x -> Q + // find a orthogonal matrix Q st. + // Qx=y + // y is [||x||,0,0,...] + + // quick ref + var sum = jStat.sum; + var range = jStat.arange; + + function qr2(x) { + // quick impletation + // https://www.stat.wisc.edu/~larget/math496/qr.html + + var n = x.length; + var p = x[0].length; + + var r = jStat.zeros(p, p); + x = jStat.copy(x); + + var i,j,k; + for(j = 0; j < p; j++){ + r[j][j] = Math.sqrt(sum(range(n).map(function(i){ + return x[i][j] * x[i][j]; + }))); + for(i = 0; i < n; i++){ + x[i][j] = x[i][j] / r[j][j]; + } + for(k = j+1; k < p; k++){ + r[j][k] = sum(range(n).map(function(i){ + return x[i][j] * x[i][k]; + })); + for(i = 0; i < n; i++){ + x[i][k] = x[i][k] - x[i][j]*r[j][k]; + } + } + } + return [x, r]; + } + + return qr2; + }()), + + lstsq: (function() { + // solve least squard problem for Ax=b as QR decomposition way if b is + // [[b1],[b2],[b3]] form will return [[x1],[x2],[x3]] array form solution + // else b is [b1,b2,b3] form will return [x1,x2,x3] array form solution + function R_I(A) { + A = jStat.copy(A); + var size = A.length; + var I = jStat.identity(size); + jStat.arange(size - 1, -1, -1).forEach(function(i) { + jStat.sliceAssign( + I, { row: i }, jStat.divide(jStat.slice(I, { row: i }), A[i][i])); + jStat.sliceAssign( + A, { row: i }, jStat.divide(jStat.slice(A, { row: i }), A[i][i])); + jStat.arange(i).forEach(function(j) { + var c = jStat.multiply(A[j][i], -1); + var Aj = jStat.slice(A, { row: j }); + var cAi = jStat.multiply(jStat.slice(A, { row: i }), c); + jStat.sliceAssign(A, { row: j }, jStat.add(Aj, cAi)); + var Ij = jStat.slice(I, { row: j }); + var cIi = jStat.multiply(jStat.slice(I, { row: i }), c); + jStat.sliceAssign(I, { row: j }, jStat.add(Ij, cIi)); + }) + }); + return I; + } + + function qr_solve(A, b){ + var array_mode = false; + if (b[0].length === undefined) { + // [c1,c2,c3] mode + b = b.map(function(x){ return [x] }); + array_mode = true; + } + var QR = jStat.QR(A); + var Q = QR[0]; + var R = QR[1]; + var attrs = A[0].length; + var Q1 = jStat.slice(Q,{col:{end:attrs}}); + var R1 = jStat.slice(R,{row:{end:attrs}}); + var RI = R_I(R1); + var Q2 = jStat.transpose(Q1); + + if(Q2[0].length === undefined){ + Q2 = [Q2]; // The confusing jStat.multifly implementation threat nature process again. + } + + var x = jStat.multiply(jStat.multiply(RI, Q2), b); + + if(x.length === undefined){ + x = [[x]]; // The confusing jStat.multifly implementation threat nature process again. + } + + + if (array_mode) + return x.map(function(i){ return i[0] }); + return x; + } + + return qr_solve; + }()), + + jacobi: function jacobi(a) { + var condition = 1; + var n = a.length; + var e = jStat.identity(n, n); + var ev = []; + var b, i, j, p, q, maxim, theta, s; + // condition === 1 only if tolerance is not reached + while (condition === 1) { + maxim = a[0][1]; + p = 0; + q = 1; + for (i = 0; i < n; i++) { + for (j = 0; j < n; j++) { + if (i != j) { + if (maxim < Math.abs(a[i][j])) { + maxim = Math.abs(a[i][j]); + p = i; + q = j; + } + } + } + } + if (a[p][p] === a[q][q]) + theta = (a[p][q] > 0) ? Math.PI / 4 : -Math.PI / 4; + else + theta = Math.atan(2 * a[p][q] / (a[p][p] - a[q][q])) / 2; + s = jStat.identity(n, n); + s[p][p] = Math.cos(theta); + s[p][q] = -Math.sin(theta); + s[q][p] = Math.sin(theta); + s[q][q] = Math.cos(theta); + // eigen vector matrix + e = jStat.multiply(e, s); + b = jStat.multiply(jStat.multiply(jStat.inv(s), a), s); + a = b; + condition = 0; + for (i = 1; i < n; i++) { + for (j = 1; j < n; j++) { + if (i != j && Math.abs(a[i][j]) > 0.001) { + condition = 1; + } + } + } + } + for (i = 0; i < n; i++) ev.push(a[i][i]); + //returns both the eigenvalue and eigenmatrix + return [e, ev]; + }, + + rungekutta: function rungekutta(f, h, p, t_j, u_j, order) { + var k1, k2, u_j1, k3, k4; + if (order === 2) { + while (t_j <= p) { + k1 = h * f(t_j, u_j); + k2 = h * f(t_j + h, u_j + k1); + u_j1 = u_j + (k1 + k2) / 2; + u_j = u_j1; + t_j = t_j + h; + } + } + if (order === 4) { + while (t_j <= p) { + k1 = h * f(t_j, u_j); + k2 = h * f(t_j + h / 2, u_j + k1 / 2); + k3 = h * f(t_j + h / 2, u_j + k2 / 2); + k4 = h * f(t_j +h, u_j + k3); + u_j1 = u_j + (k1 + 2 * k2 + 2 * k3 + k4) / 6; + u_j = u_j1; + t_j = t_j + h; + } + } + return u_j; + }, + + romberg: function romberg(f, a, b, order) { + var i = 0; + var h = (b - a) / 2; + var x = []; + var h1 = []; + var g = []; + var m, a1, j, k, I; + while (i < order / 2) { + I = f(a); + for (j = a, k = 0; j <= b; j = j + h, k++) x[k] = j; + m = x.length; + for (j = 1; j < m - 1; j++) { + I += (((j % 2) !== 0) ? 4 : 2) * f(x[j]); + } + I = (h / 3) * (I + f(b)); + g[i] = I; + h /= 2; + i++; + } + a1 = g.length; + m = 1; + while (a1 !== 1) { + for (j = 0; j < a1 - 1; j++) + h1[j] = ((Math.pow(4, m)) * g[j + 1] - g[j]) / (Math.pow(4, m) - 1); + a1 = h1.length; + g = h1; + h1 = []; + m++; + } + return g; + }, + + richardson: function richardson(X, f, x, h) { + function pos(X, x) { + var i = 0; + var n = X.length; + var p; + for (; i < n; i++) + if (X[i] === x) p = i; + return p; + } + var h_min = Math.abs(x - X[pos(X, x) + 1]); + var i = 0; + var g = []; + var h1 = []; + var y1, y2, m, a, j; + while (h >= h_min) { + y1 = pos(X, x + h); + y2 = pos(X, x); + g[i] = (f[y1] - 2 * f[y2] + f[2 * y2 - y1]) / (h * h); + h /= 2; + i++; + } + a = g.length; + m = 1; + while (a != 1) { + for (j = 0; j < a - 1; j++) + h1[j] = ((Math.pow(4, m)) * g[j + 1] - g[j]) / (Math.pow(4, m) - 1); + a = h1.length; + g = h1; + h1 = []; + m++; + } + return g; + }, + + simpson: function simpson(f, a, b, n) { + var h = (b - a) / n; + var I = f(a); + var x = []; + var j = a; + var k = 0; + var i = 1; + var m; + for (; j <= b; j = j + h, k++) + x[k] = j; + m = x.length; + for (; i < m - 1; i++) { + I += ((i % 2 !== 0) ? 4 : 2) * f(x[i]); + } + return (h / 3) * (I + f(b)); + }, + + hermite: function hermite(X, F, dF, value) { + var n = X.length; + var p = 0; + var i = 0; + var l = []; + var dl = []; + var A = []; + var B = []; + var j; + for (; i < n; i++) { + l[i] = 1; + for (j = 0; j < n; j++) { + if (i != j) l[i] *= (value - X[j]) / (X[i] - X[j]); + } + dl[i] = 0; + for (j = 0; j < n; j++) { + if (i != j) dl[i] += 1 / (X [i] - X[j]); + } + A[i] = (1 - 2 * (value - X[i]) * dl[i]) * (l[i] * l[i]); + B[i] = (value - X[i]) * (l[i] * l[i]); + p += (A[i] * F[i] + B[i] * dF[i]); + } + return p; + }, + + lagrange: function lagrange(X, F, value) { + var p = 0; + var i = 0; + var j, l; + var n = X.length; + for (; i < n; i++) { + l = F[i]; + for (j = 0; j < n; j++) { + // calculating the lagrange polynomial L_i + if (i != j) l *= (value - X[j]) / (X[i] - X[j]); + } + // adding the lagrange polynomials found above + p += l; + } + return p; + }, + + cubic_spline: function cubic_spline(X, F, value) { + var n = X.length; + var i = 0, j; + var A = []; + var B = []; + var alpha = []; + var c = []; + var h = []; + var b = []; + var d = []; + for (; i < n - 1; i++) + h[i] = X[i + 1] - X[i]; + alpha[0] = 0; + for (i = 1; i < n - 1; i++) { + alpha[i] = (3 / h[i]) * (F[i + 1] - F[i]) - + (3 / h[i-1]) * (F[i] - F[i-1]); + } + for (i = 1; i < n - 1; i++) { + A[i] = []; + B[i] = []; + A[i][i-1] = h[i-1]; + A[i][i] = 2 * (h[i - 1] + h[i]); + A[i][i+1] = h[i]; + B[i][0] = alpha[i]; + } + c = jStat.multiply(jStat.inv(A), B); + for (j = 0; j < n - 1; j++) { + b[j] = (F[j + 1] - F[j]) / h[j] - h[j] * (c[j + 1][0] + 2 * c[j][0]) / 3; + d[j] = (c[j + 1][0] - c[j][0]) / (3 * h[j]); + } + for (j = 0; j < n; j++) { + if (X[j] > value) break; + } + j -= 1; + return F[j] + (value - X[j]) * b[j] + jStat.sq(value-X[j]) * + c[j] + (value - X[j]) * jStat.sq(value - X[j]) * d[j]; + }, + + gauss_quadrature: function gauss_quadrature() { + throw new Error('gauss_quadrature not yet implemented'); + }, + + PCA: function PCA(X) { + var m = X.length; + var n = X[0].length; + var i = 0; + var j, temp1; + var u = []; + var D = []; + var result = []; + var temp2 = []; + var Y = []; + var Bt = []; + var B = []; + var C = []; + var V = []; + var Vt = []; + for (i = 0; i < m; i++) { + u[i] = jStat.sum(X[i]) / n; + } + for (i = 0; i < n; i++) { + B[i] = []; + for(j = 0; j < m; j++) { + B[i][j] = X[j][i] - u[j]; + } + } + B = jStat.transpose(B); + for (i = 0; i < m; i++) { + C[i] = []; + for (j = 0; j < m; j++) { + C[i][j] = (jStat.dot([B[i]], [B[j]])) / (n - 1); + } + } + result = jStat.jacobi(C); + V = result[0]; + D = result[1]; + Vt = jStat.transpose(V); + for (i = 0; i < D.length; i++) { + for (j = i; j < D.length; j++) { + if(D[i] < D[j]) { + temp1 = D[i]; + D[i] = D[j]; + D[j] = temp1; + temp2 = Vt[i]; + Vt[i] = Vt[j]; + Vt[j] = temp2; + } + } + } + Bt = jStat.transpose(B); + for (i = 0; i < m; i++) { + Y[i] = []; + for (j = 0; j < Bt.length; j++) { + Y[i][j] = jStat.dot([Vt[i]], [Bt[j]]); + } + } + return [X, D, Vt, Y]; + } +}); + +// extend jStat.fn with methods that require one argument +(function(funcs) { + for (var i = 0; i < funcs.length; i++) (function(passfunc) { + jStat.fn[passfunc] = function(arg, func) { + var tmpthis = this; + // check for callback + if (func) { + setTimeout(function() { + func.call(tmpthis, jStat.fn[passfunc].call(tmpthis, arg)); + }, 15); + return this; + } + if (typeof jStat[passfunc](this, arg) === 'number') + return jStat[passfunc](this, arg); + else + return jStat(jStat[passfunc](this, arg)); + }; + }(funcs[i])); +}('add divide multiply subtract dot pow exp log abs norm angle'.split(' '))); + +}(jStat, Math)); +(function(jStat, Math) { + +var slice = [].slice; +var isNumber = jStat.utils.isNumber; +var isArray = jStat.utils.isArray; + +// flag==true denotes use of sample standard deviation +// Z Statistics +jStat.extend({ + // 2 different parameter lists: + // (value, mean, sd) + // (value, array, flag) + zscore: function zscore() { + var args = slice.call(arguments); + if (isNumber(args[1])) { + return (args[0] - args[1]) / args[2]; + } + return (args[0] - jStat.mean(args[1])) / jStat.stdev(args[1], args[2]); + }, + + // 3 different paramter lists: + // (value, mean, sd, sides) + // (zscore, sides) + // (value, array, sides, flag) + ztest: function ztest() { + var args = slice.call(arguments); + var z; + if (isArray(args[1])) { + // (value, array, sides, flag) + z = jStat.zscore(args[0],args[1],args[3]); + return (args[2] === 1) ? + (jStat.normal.cdf(-Math.abs(z), 0, 1)) : + (jStat.normal.cdf(-Math.abs(z), 0, 1)*2); + } else { + if (args.length > 2) { + // (value, mean, sd, sides) + z = jStat.zscore(args[0],args[1],args[2]); + return (args[3] === 1) ? + (jStat.normal.cdf(-Math.abs(z),0,1)) : + (jStat.normal.cdf(-Math.abs(z),0,1)* 2); + } else { + // (zscore, sides) + z = args[0]; + return (args[1] === 1) ? + (jStat.normal.cdf(-Math.abs(z),0,1)) : + (jStat.normal.cdf(-Math.abs(z),0,1)*2); + } + } + } +}); + +jStat.extend(jStat.fn, { + zscore: function zscore(value, flag) { + return (value - this.mean()) / this.stdev(flag); + }, + + ztest: function ztest(value, sides, flag) { + var zscore = Math.abs(this.zscore(value, flag)); + return (sides === 1) ? + (jStat.normal.cdf(-zscore, 0, 1)) : + (jStat.normal.cdf(-zscore, 0, 1) * 2); + } +}); + +// T Statistics +jStat.extend({ + // 2 parameter lists + // (value, mean, sd, n) + // (value, array) + tscore: function tscore() { + var args = slice.call(arguments); + return (args.length === 4) ? + ((args[0] - args[1]) / (args[2] / Math.sqrt(args[3]))) : + ((args[0] - jStat.mean(args[1])) / + (jStat.stdev(args[1], true) / Math.sqrt(args[1].length))); + }, + + // 3 different paramter lists: + // (value, mean, sd, n, sides) + // (tscore, n, sides) + // (value, array, sides) + ttest: function ttest() { + var args = slice.call(arguments); + var tscore; + if (args.length === 5) { + tscore = Math.abs(jStat.tscore(args[0], args[1], args[2], args[3])); + return (args[4] === 1) ? + (jStat.studentt.cdf(-tscore, args[3]-1)) : + (jStat.studentt.cdf(-tscore, args[3]-1)*2); + } + if (isNumber(args[1])) { + tscore = Math.abs(args[0]) + return (args[2] == 1) ? + (jStat.studentt.cdf(-tscore, args[1]-1)) : + (jStat.studentt.cdf(-tscore, args[1]-1) * 2); + } + tscore = Math.abs(jStat.tscore(args[0], args[1])) + return (args[2] == 1) ? + (jStat.studentt.cdf(-tscore, args[1].length-1)) : + (jStat.studentt.cdf(-tscore, args[1].length-1) * 2); + } +}); + +jStat.extend(jStat.fn, { + tscore: function tscore(value) { + return (value - this.mean()) / (this.stdev(true) / Math.sqrt(this.cols())); + }, + + ttest: function ttest(value, sides) { + return (sides === 1) ? + (1 - jStat.studentt.cdf(Math.abs(this.tscore(value)), this.cols()-1)) : + (jStat.studentt.cdf(-Math.abs(this.tscore(value)), this.cols()-1)*2); + } +}); + +// F Statistics +jStat.extend({ + // Paramter list is as follows: + // (array1, array2, array3, ...) + // or it is an array of arrays + // array of arrays conversion + anovafscore: function anovafscore() { + var args = slice.call(arguments), + expVar, sample, sampMean, sampSampMean, tmpargs, unexpVar, i, j; + if (args.length === 1) { + tmpargs = new Array(args[0].length); + for (i = 0; i < args[0].length; i++) { + tmpargs[i] = args[0][i]; + } + args = tmpargs; + } + // Builds sample array + sample = new Array(); + for (i = 0; i < args.length; i++) { + sample = sample.concat(args[i]); + } + sampMean = jStat.mean(sample); + // Computes the explained variance + expVar = 0; + for (i = 0; i < args.length; i++) { + expVar = expVar + args[i].length * Math.pow(jStat.mean(args[i]) - sampMean, 2); + } + expVar /= (args.length - 1); + // Computes unexplained variance + unexpVar = 0; + for (i = 0; i < args.length; i++) { + sampSampMean = jStat.mean(args[i]); + for (j = 0; j < args[i].length; j++) { + unexpVar += Math.pow(args[i][j] - sampSampMean, 2); + } + } + unexpVar /= (sample.length - args.length); + return expVar / unexpVar; + }, + + // 2 different paramter setups + // (array1, array2, array3, ...) + // (anovafscore, df1, df2) + anovaftest: function anovaftest() { + var args = slice.call(arguments), + df1, df2, n, i; + if (isNumber(args[0])) { + return 1 - jStat.centralF.cdf(args[0], args[1], args[2]); + } + var anovafscore = jStat.anovafscore(args); + df1 = args.length - 1; + n = 0; + for (i = 0; i < args.length; i++) { + n = n + args[i].length; + } + df2 = n - df1 - 1; + return 1 - jStat.centralF.cdf(anovafscore, df1, df2); + }, + + ftest: function ftest(fscore, df1, df2) { + return 1 - jStat.centralF.cdf(fscore, df1, df2); + } +}); + +jStat.extend(jStat.fn, { + anovafscore: function anovafscore() { + return jStat.anovafscore(this.toArray()); + }, + + anovaftes: function anovaftes() { + var n = 0; + var i; + for (i = 0; i < this.length; i++) { + n = n + this[i].length; + } + return jStat.ftest(this.anovafscore(), this.length - 1, n - this.length); + } +}); + +// Tukey's range test +jStat.extend({ + // 2 parameter lists + // (mean1, mean2, n1, n2, sd) + // (array1, array2, sd) + qscore: function qscore() { + var args = slice.call(arguments); + var mean1, mean2, n1, n2, sd; + if (isNumber(args[0])) { + mean1 = args[0]; + mean2 = args[1]; + n1 = args[2]; + n2 = args[3]; + sd = args[4]; + } else { + mean1 = jStat.mean(args[0]); + mean2 = jStat.mean(args[1]); + n1 = args[0].length; + n2 = args[1].length; + sd = args[2]; + } + return Math.abs(mean1 - mean2) / (sd * Math.sqrt((1 / n1 + 1 / n2) / 2)); + }, + + // 3 different parameter lists: + // (qscore, n, k) + // (mean1, mean2, n1, n2, sd, n, k) + // (array1, array2, sd, n, k) + qtest: function qtest() { + var args = slice.call(arguments); + + var qscore; + if (args.length === 3) { + qscore = args[0]; + args = args.slice(1); + } else if (args.length === 7) { + qscore = jStat.qscore(args[0], args[1], args[2], args[3], args[4]); + args = args.slice(5); + } else { + qscore = jStat.qscore(args[0], args[1], args[2]); + args = args.slice(3); + } + + var n = args[0]; + var k = args[1]; + + return 1 - jStat.tukey.cdf(qscore, k, n - k); + }, + + tukeyhsd: function tukeyhsd(arrays) { + var sd = jStat.pooledstdev(arrays); + var means = arrays.map(function (arr) {return jStat.mean(arr);}); + var n = arrays.reduce(function (n, arr) {return n + arr.length;}, 0); + + var results = []; + for (var i = 0; i < arrays.length; ++i) { + for (var j = i + 1; j < arrays.length; ++j) { + var p = jStat.qtest(means[i], means[j], arrays[i].length, arrays[j].length, sd, n, arrays.length); + results.push([[i, j], p]); + } + } + + return results; + } +}); + +// Error Bounds +jStat.extend({ + // 2 different parameter setups + // (value, alpha, sd, n) + // (value, alpha, array) + normalci: function normalci() { + var args = slice.call(arguments), + ans = new Array(2), + change; + if (args.length === 4) { + change = Math.abs(jStat.normal.inv(args[1] / 2, 0, 1) * + args[2] / Math.sqrt(args[3])); + } else { + change = Math.abs(jStat.normal.inv(args[1] / 2, 0, 1) * + jStat.stdev(args[2]) / Math.sqrt(args[2].length)); + } + ans[0] = args[0] - change; + ans[1] = args[0] + change; + return ans; + }, + + // 2 different parameter setups + // (value, alpha, sd, n) + // (value, alpha, array) + tci: function tci() { + var args = slice.call(arguments), + ans = new Array(2), + change; + if (args.length === 4) { + change = Math.abs(jStat.studentt.inv(args[1] / 2, args[3] - 1) * + args[2] / Math.sqrt(args[3])); + } else { + change = Math.abs(jStat.studentt.inv(args[1] / 2, args[2].length - 1) * + jStat.stdev(args[2], true) / Math.sqrt(args[2].length)); + } + ans[0] = args[0] - change; + ans[1] = args[0] + change; + return ans; + }, + + significant: function significant(pvalue, alpha) { + return pvalue < alpha; + } +}); + +jStat.extend(jStat.fn, { + normalci: function normalci(value, alpha) { + return jStat.normalci(value, alpha, this.toArray()); + }, + + tci: function tci(value, alpha) { + return jStat.tci(value, alpha, this.toArray()); + } +}); + +// internal method for calculating the z-score for a difference of proportions test +function differenceOfProportions(p1, n1, p2, n2) { + if (p1 > 1 || p2 > 1 || p1 <= 0 || p2 <= 0) { + throw new Error("Proportions should be greater than 0 and less than 1") + } + var pooled = (p1 * n1 + p2 * n2) / (n1 + n2); + var se = Math.sqrt(pooled * (1 - pooled) * ((1/n1) + (1/n2))); + return (p1 - p2) / se; +} + +// Difference of Proportions +jStat.extend(jStat.fn, { + oneSidedDifferenceOfProportions: function oneSidedDifferenceOfProportions(p1, n1, p2, n2) { + var z = differenceOfProportions(p1, n1, p2, n2); + return jStat.ztest(z, 1); + }, + + twoSidedDifferenceOfProportions: function twoSidedDifferenceOfProportions(p1, n1, p2, n2) { + var z = differenceOfProportions(p1, n1, p2, n2); + return jStat.ztest(z, 2); + } +}); + +}(jStat, Math)); +jStat.models = (function(){ + function sub_regress(exog) { + var var_count = exog[0].length; + var modelList = jStat.arange(var_count).map(function(endog_index) { + var exog_index = + jStat.arange(var_count).filter(function(i){return i!==endog_index}); + return ols(jStat.col(exog, endog_index).map(function(x){ return x[0] }), + jStat.col(exog, exog_index)) + }); + return modelList; + } + + // do OLS model regress + // exog have include const columns ,it will not generate it .In fact, exog is + // "design matrix" look at + //https://en.wikipedia.org/wiki/Design_matrix + function ols(endog, exog) { + var nobs = endog.length; + var df_model = exog[0].length - 1; + var df_resid = nobs-df_model - 1; + var coef = jStat.lstsq(exog, endog); + var predict = + jStat.multiply(exog, coef.map(function(x) { return [x] })) + .map(function(p) { return p[0] }); + var resid = jStat.subtract(endog, predict); + var ybar = jStat.mean(endog); + // constant cause problem + // var SST = jStat.sum(endog.map(function(y) { + // return Math.pow(y-ybar,2); + // })); + var SSE = jStat.sum(predict.map(function(f) { + return Math.pow(f - ybar, 2); + })); + var SSR = jStat.sum(endog.map(function(y, i) { + return Math.pow(y - predict[i], 2); + })); + var SST = SSE + SSR; + var R2 = (SSE / SST); + return { + exog:exog, + endog:endog, + nobs:nobs, + df_model:df_model, + df_resid:df_resid, + coef:coef, + predict:predict, + resid:resid, + ybar:ybar, + SST:SST, + SSE:SSE, + SSR:SSR, + R2:R2 + }; + } + + // H0: b_I=0 + // H1: b_I!=0 + function t_test(model) { + var subModelList = sub_regress(model.exog); + //var sigmaHat=jStat.stdev(model.resid); + var sigmaHat = Math.sqrt(model.SSR / (model.df_resid)); + var seBetaHat = subModelList.map(function(mod) { + var SST = mod.SST; + var R2 = mod.R2; + return sigmaHat / Math.sqrt(SST * (1 - R2)); + }); + var tStatistic = model.coef.map(function(coef, i) { + return (coef - 0) / seBetaHat[i]; + }); + var pValue = tStatistic.map(function(t) { + var leftppf = jStat.studentt.cdf(t, model.df_resid); + return (leftppf > 0.5 ? 1 - leftppf : leftppf) * 2; + }); + var c = jStat.studentt.inv(0.975, model.df_resid); + var interval95 = model.coef.map(function(coef, i) { + var d = c * seBetaHat[i]; + return [coef - d, coef + d]; + }) + return { + se: seBetaHat, + t: tStatistic, + p: pValue, + sigmaHat: sigmaHat, + interval95: interval95 + }; + } + + function F_test(model) { + var F_statistic = + (model.R2 / model.df_model) / ((1 - model.R2) / model.df_resid); + var fcdf = function(x, n1, n2) { + return jStat.beta.cdf(x / (n2 / n1 + x), n1 / 2, n2 / 2) + } + var pvalue = 1 - fcdf(F_statistic, model.df_model, model.df_resid); + return { F_statistic: F_statistic, pvalue: pvalue }; + } + + function ols_wrap(endog, exog) { + var model = ols(endog,exog); + var ttest = t_test(model); + var ftest = F_test(model); + // Provide the Wherry / Ezekiel / McNemar / Cohen Adjusted R^2 + // Which matches the 'adjusted R^2' provided by R's lm package + var adjust_R2 = + 1 - (1 - model.R2) * ((model.nobs - 1) / (model.df_resid)); + model.t = ttest; + model.f = ftest; + model.adjust_R2 = adjust_R2; + return model; + } + + return { ols: ols_wrap }; +})(); +//To regress, simply build X matrix +//(append column of 1's) using +//buildxmatrix and build the Y +//matrix using buildymatrix +//(simply the transpose) +//and run regress. + + + +//Regressions + +jStat.extend({ + buildxmatrix: function buildxmatrix(){ + //Parameters will be passed in as such + //(array1,array2,array3,...) + //as (x1,x2,x3,...) + //needs to be (1,x1,x2,x3,...) + var matrixRows = new Array(arguments.length); + for(var i=0;i1){ + result = []; + for (i = 0; i < A.rows(); i++) { + result[i] = []; + for (j = 0; j < B.cols(); j++) { + sum = 0; + for (k = 0; k < A.cols(); k++) { + sum += A.toArray()[i][k] * B.toArray()[k][j]; + } + result[i][j] = sum; + } + } + return jStat(result); + } + result = []; + for (i = 0; i < A.rows(); i++) { + result[i] = []; + for (j = 0; j < B.cols(); j++) { + sum = 0; + for (k = 0; k < A.cols(); k++) { + sum += A.toArray()[i][k] * B.toArray()[j]; + } + result[i][j] = sum; + } + } + return jStat(result); + } + }, + + //regress and regresst to be fixed + + regress: function regress(jMatX,jMatY){ + //print("regressin!"); + //print(jMatX.toArray()); + var innerinv = jStat.xtranspxinv(jMatX); + //print(innerinv); + var xtransp = jMatX.transpose(); + var next = jStat.matrixmult(jStat(innerinv),xtransp); + return jStat.matrixmult(next,jMatY); + + }, + + regresst: function regresst(jMatX,jMatY,sides){ + var beta = jStat.regress(jMatX,jMatY); + + var compile = {}; + compile.anova = {}; + var jMatYBar = jStat.jMatYBar(jMatX, beta); + compile.yBar = jMatYBar; + var yAverage = jMatY.mean(); + compile.anova.residuals = jStat.residuals(jMatY, jMatYBar); + + compile.anova.ssr = jStat.ssr(jMatYBar, yAverage); + compile.anova.msr = compile.anova.ssr / (jMatX[0].length - 1); + + compile.anova.sse = jStat.sse(jMatY, jMatYBar); + compile.anova.mse = + compile.anova.sse / (jMatY.length - (jMatX[0].length - 1) - 1); + + compile.anova.sst = jStat.sst(jMatY, yAverage); + compile.anova.mst = compile.anova.sst / (jMatY.length - 1); + + compile.anova.r2 = 1 - (compile.anova.sse / compile.anova.sst); + if (compile.anova.r2 < 0) compile.anova.r2 = 0; + + compile.anova.fratio = compile.anova.msr / compile.anova.mse; + compile.anova.pvalue = + jStat.anovaftest(compile.anova.fratio, + jMatX[0].length - 1, + jMatY.length - (jMatX[0].length - 1) - 1); + + compile.anova.rmse = Math.sqrt(compile.anova.mse); + + compile.anova.r2adj = 1 - (compile.anova.mse / compile.anova.mst); + if (compile.anova.r2adj < 0) compile.anova.r2adj = 0; + + compile.stats = new Array(jMatX[0].length); + var covar = jStat.xtranspxinv(jMatX); + var sds, ts, ps; + + for(var i=0; im?n:m;return Math.pow(10,17-~~(Math.log(val>0?val:-val)*Math.LOG10E))}var isArray=Array.isArray||function isArray(arg){return toString.call(arg)==="[object Array]"};function isFunction(arg){return toString.call(arg)==="[object Function]"}function isNumber(num){return typeof num==="number"?num-num===0:false}function toVector(arr){return concat.apply([],arr)}function jStat(){return new jStat._init(arguments)}jStat.fn=jStat.prototype;jStat._init=function _init(args){if(isArray(args[0])){if(isArray(args[0][0])){if(isFunction(args[1]))args[0]=jStat.map(args[0],args[1]);for(var i=0;i=0;nrow--,i++)res[i]=[arr[i][nrow]];return res};jStat.transpose=function transpose(arr){var obj=[];var objArr,rows,cols,j,i;if(!isArray(arr[0]))arr=[arr];rows=arr.length;cols=arr[0].length;for(i=0;i0)res[row][0]=arr[row][0];for(col=1;colend&&step>0){return[]}if(step>0){for(i=start;iend;i+=step){rl.push(i)}}return rl};jStat.slice=function(){function _slice(list,start,end,step){var i;var rl=[];var length=list.length;if(start===undefined&&end===undefined&&step===undefined){return jStat.copy(list)}start=start||0;end=end||list.length;start=start>=0?start:length+start;end=end>=0?end:length+end;step=step||1;if(start===end||step===0){return[]}if(startend&&step>0){return[]}if(step>0){for(i=start;iend;i+=step){rl.push(list[i])}}return rl}function slice(list,rcSlice){var colSlice,rowSlice;rcSlice=rcSlice||{};if(isNumber(rcSlice.row)){if(isNumber(rcSlice.col))return list[rcSlice.row][rcSlice.col];var row=jStat.rowa(list,rcSlice.row);colSlice=rcSlice.col||{};return _slice(row,colSlice.start,colSlice.end,colSlice.step)}if(isNumber(rcSlice.col)){var col=jStat.cola(list,rcSlice.col);rowSlice=rcSlice.row||{};return _slice(col,rowSlice.start,rowSlice.end,rowSlice.step)}rowSlice=rcSlice.row||{};colSlice=rcSlice.col||{};var rows=_slice(list,rowSlice.start,rowSlice.end,rowSlice.step);return rows.map(function(row){return _slice(row,colSlice.start,colSlice.end,colSlice.step)})}return slice}();jStat.sliceAssign=function sliceAssign(A,rcSlice,B){var nl,ml;if(isNumber(rcSlice.row)){if(isNumber(rcSlice.col))return A[rcSlice.row][rcSlice.col]=B;rcSlice.col=rcSlice.col||{};rcSlice.col.start=rcSlice.col.start||0;rcSlice.col.end=rcSlice.col.end||A[0].length;rcSlice.col.step=rcSlice.col.step||1;nl=jStat.arange(rcSlice.col.start,Math.min(A.length,rcSlice.col.end),rcSlice.col.step);var m=rcSlice.row;nl.forEach(function(n,i){A[m][n]=B[i]});return A}if(isNumber(rcSlice.col)){rcSlice.row=rcSlice.row||{};rcSlice.row.start=rcSlice.row.start||0;rcSlice.row.end=rcSlice.row.end||A.length;rcSlice.row.step=rcSlice.row.step||1;ml=jStat.arange(rcSlice.row.start,Math.min(A[0].length,rcSlice.row.end),rcSlice.row.step);var n=rcSlice.col;ml.forEach(function(m,j){A[m][n]=B[j]});return A}if(B[0].length===undefined){B=[B]}rcSlice.row.start=rcSlice.row.start||0;rcSlice.row.end=rcSlice.row.end||A.length;rcSlice.row.step=rcSlice.row.step||1;rcSlice.col.start=rcSlice.col.start||0;rcSlice.col.end=rcSlice.col.end||A[0].length;rcSlice.col.step=rcSlice.col.step||1;ml=jStat.arange(rcSlice.row.start,Math.min(A.length,rcSlice.row.end),rcSlice.row.step);nl=jStat.arange(rcSlice.col.start,Math.min(A[0].length,rcSlice.col.end),rcSlice.col.step);ml.forEach(function(m,i){nl.forEach(function(n,j){A[m][n]=B[i][j]})});return A};jStat.diagonal=function diagonal(diagArray){var mat=jStat.zeros(diagArray.length,diagArray.length);diagArray.forEach(function(t,i){mat[i][i]=t});return mat};jStat.copy=function copy(A){return A.map(function(row){if(isNumber(row))return row;return row.map(function(t){return t})})};var jProto=jStat.prototype;jProto.length=0;jProto.push=Array.prototype.push;jProto.sort=Array.prototype.sort;jProto.splice=Array.prototype.splice;jProto.slice=Array.prototype.slice;jProto.toArray=function toArray(){return this.length>1?slice.call(this):slice.call(this)[0]};jProto.map=function map(func,toAlter){return jStat(jStat.map(this,func,toAlter))};jProto.cumreduce=function cumreduce(func,toAlter){return jStat(jStat.cumreduce(this,func,toAlter))};jProto.alter=function alter(func){jStat.alter(this,func);return this};(function(funcs){for(var i=0;i=0)sum+=arr[i];return sum};jStat.sumsqrd=function sumsqrd(arr){var sum=0;var i=arr.length;while(--i>=0)sum+=arr[i]*arr[i];return sum};jStat.sumsqerr=function sumsqerr(arr){var mean=jStat.mean(arr);var sum=0;var i=arr.length;var tmp;while(--i>=0){tmp=arr[i]-mean;sum+=tmp*tmp}return sum};jStat.sumrow=function sumrow(arr){var sum=0;var i=arr.length;while(--i>=0)sum+=arr[i];return sum};jStat.product=function product(arr){var prod=1;var i=arr.length;while(--i>=0)prod*=arr[i];return prod};jStat.min=function min(arr){var low=arr[0];var i=0;while(++ihigh)high=arr[i];return high};jStat.unique=function unique(arr){var hash={},_arr=[];for(var i=0;imaxCount){mode_arr=[_arr[i]];maxCount=count;numMaxCount=0}else if(count===maxCount){mode_arr.push(_arr[i]);numMaxCount++}count=1}}return numMaxCount===0?mode_arr[0]:mode_arr};jStat.range=function range(arr){return jStat.max(arr)-jStat.min(arr)};jStat.variance=function variance(arr,flag){return jStat.sumsqerr(arr)/(arr.length-(flag?1:0))};jStat.pooledvariance=function pooledvariance(arr){var sumsqerr=arr.reduce(function(a,samples){return a+jStat.sumsqerr(samples)},0);var count=arr.reduce(function(a,samples){return a+samples.length},0);return sumsqerr/(count-arr.length)};jStat.deviation=function(arr){var mean=jStat.mean(arr);var arrlen=arr.length;var dev=new Array(arrlen);for(var i=0;i=0;i--){a.push(Math.abs(arr[i]-mean))}return jStat.mean(a)};jStat.meddev=function meddev(arr){var median=jStat.median(arr);var a=[];for(var i=arr.length-1;i>=0;i--){a.push(Math.abs(arr[i]-median))}return jStat.median(a)};jStat.coeffvar=function coeffvar(arr){return jStat.stdev(arr)/jStat.mean(arr)};jStat.quartiles=function quartiles(arr){var arrlen=arr.length;var _arr=arr.slice().sort(ascNum);return[_arr[Math.round(arrlen/4)-1],_arr[Math.round(arrlen/2)-1],_arr[Math.round(arrlen*3/4)-1]]};jStat.quantiles=function quantiles(arr,quantilesArray,alphap,betap){var sortedArray=arr.slice().sort(ascNum);var quantileVals=[quantilesArray.length];var n=arr.length;var i,p,m,aleph,k,gamma;if(typeof alphap==="undefined")alphap=3/8;if(typeof betap==="undefined")betap=3/8;for(i=0;i1){tmpthis=fullbool===true?this:this.transpose();for(;i1){if(passfunc!=="sumrow")tmpthis=fullbool===true?this:this.transpose();for(;i1){tmpthis=tmpthis.transpose();for(;i=0;k--){gl0*=x2;gl0+=a[k]}gl=gl0/x0+.5*Math.log(xp)+(x0-.5)*Math.log(x0)-x0;if(x<=7){for(k=1;k<=n;k++){gl-=Math.log(x0-1);x0-=1}}return gl};jStat.gammafn=function gammafn(x){var p=[-1.716185138865495,24.76565080557592,-379.80425647094563,629.3311553128184,866.9662027904133,-31451.272968848367,-36144.413418691176,66456.14382024054];var q=[-30.8402300119739,315.35062697960416,-1015.1563674902192,-3107.771671572311,22538.11842098015,4755.846277527881,-134659.9598649693,-115132.2596755535];var fact=false;var n=0;var xden=0;var xnum=0;var y=x;var i,z,yi,res;if(x>171.6243769536076){return Infinity}if(y<=0){res=y%1+36e-17;if(res){fact=(!(y&1)?1:-1)*Math.PI/Math.sin(Math.PI*res);y=1-y}else{return Infinity}}yi=y;if(y<1){z=y++}else{z=(y-=n=(y|0)-1)-1}for(i=0;i<8;++i){xnum=(xnum+p[i])*z;xden=xden*z+q[i]}res=xnum/xden+1;if(yiy){for(i=0;i=1?a:1/a)*8.5+a*.4+17);var an;if(x<0||a<=0){return NaN}else if(x170||m>170?Math.exp(jStat.combinationln(n,m)):jStat.factorial(n)/jStat.factorial(m)/jStat.factorial(n-m)};jStat.combinationln=function combinationln(n,m){return jStat.factorialln(n)-jStat.factorialln(m)-jStat.factorialln(n-m)};jStat.permutation=function permutation(n,m){return jStat.factorial(n)/jStat.factorial(n-m)};jStat.betafn=function betafn(x,y){if(x<=0||y<=0)return undefined;return x+y>170?Math.exp(jStat.betaln(x,y)):jStat.gammafn(x)*jStat.gammafn(y)/jStat.gammafn(x+y)};jStat.betaln=function betaln(x,y){return jStat.gammaln(x)+jStat.gammaln(y)-jStat.gammaln(x+y)};jStat.betacf=function betacf(x,a,b){var fpmin=1e-30;var m=1;var qab=a+b;var qap=a+1;var qam=a-1;var c=1;var d=1-qab*x/qap;var m2,aa,del,h;if(Math.abs(d)=1)return Math.max(100,a+100*Math.sqrt(a));if(p<=0)return 0;if(a>1){lna1=Math.log(a1);afac=Math.exp(a1*(lna1-1)-gln);pp=p<.5?p:1-p;t=Math.sqrt(-2*Math.log(pp));x=(2.30753+t*.27061)/(1+t*(.99229+t*.04481))-t;if(p<.5)x=-x;x=Math.max(.001,a*Math.pow(1-1/(9*a)-x/(3*Math.sqrt(a)),3))}else{t=1-a*(.253+a*.12);if(p1)t=afac*Math.exp(-(x-a1)+a1*(Math.log(x)-lna1));else t=Math.exp(-x+a1*Math.log(x)-gln);u=err/t;x-=t=u/(1-.5*Math.min(1,u*((a-1)/x-1)));if(x<=0)x=.5*(x+t);if(Math.abs(t)0;j--){tmp=d;d=ty*d-dd+cof[j];dd=tmp}res=t*Math.exp(-x*x+.5*(cof[0]+ty*d)-dd);return isneg?res-1:1-res};jStat.erfc=function erfc(x){return 1-jStat.erf(x)};jStat.erfcinv=function erfcinv(p){var j=0;var x,err,t,pp;if(p>=2)return-100;if(p<=0)return 100;pp=p<1?p:2-p;t=Math.sqrt(-2*Math.log(pp/2));x=-.70711*((2.30753+t*.27061)/(1+t*(.99229+t*.04481))-t);for(;j<2;j++){err=jStat.erfc(x)-pp;x+=err/(1.1283791670955126*Math.exp(-x*x)-x*err)}return p<1?x:-x};jStat.ibetainv=function ibetainv(p,a,b){var EPS=1e-8;var a1=a-1;var b1=b-1;var j=0;var lna,lnb,pp,t,u,err,x,al,h,w,afac;if(p<=0)return 0;if(p>=1)return 1;if(a>=1&&b>=1){pp=p<.5?p:1-p;t=Math.sqrt(-2*Math.log(pp));x=(2.30753+t*.27061)/(1+t*(.99229+t*.04481))-t;if(p<.5)x=-x;al=(x*x-3)/6;h=2/(1/(2*a-1)+1/(2*b-1));w=x*Math.sqrt(al+h)/h-(1/(2*b-1)-1/(2*a-1))*(al+5/6-2/(3*h));x=a/(a+b*Math.exp(2*w))}else{lna=Math.log(a/(a+b));lnb=Math.log(b/(a+b));t=Math.exp(a*lna)/a;u=Math.exp(b*lnb)/b;w=t+u;if(p=1)x=.5*(x+t+1);if(Math.abs(t)0)break}return x};jStat.ibeta=function ibeta(x,a,b){var bt=x===0||x===1?0:Math.exp(jStat.gammaln(a+b)-jStat.gammaln(a)-jStat.gammaln(b)+a*Math.log(x)+b*Math.log(1-x));if(x<0||x>1)return false;if(x<(a+1)/(a+b+2))return bt*jStat.betacf(x,a,b)/a;return 1-bt*jStat.betacf(1-x,b,a)/b};jStat.randn=function randn(n,m){var u,v,x,y,q;if(!m)m=n;if(n)return jStat.create(n,m,function(){return jStat.randn()});do{u=jStat._random_fn();v=1.7156*(jStat._random_fn()-.5);x=u-.449871;y=Math.abs(v)+.386595;q=x*x+y*(.196*y-.25472*x)}while(q>.27597&&(q>.27846||v*v>-4*Math.log(u)*u*u));return v/u};jStat.randg=function randg(shape,n,m){var oalph=shape;var a1,a2,u,v,x,mat;if(!m)m=n;if(!shape)shape=1;if(n){mat=jStat.zeros(n,m);mat.alter(function(){return jStat.randg(shape)});return mat}if(shape<1)shape+=1;a1=shape-1/3;a2=1/Math.sqrt(9*a1);do{do{x=jStat.randn();v=1+a2*x}while(v<=0);v=v*v*v;u=jStat._random_fn()}while(u>1-.331*Math.pow(x,4)&&Math.log(u)>.5*x*x+a1*(1-v+Math.log(v)));if(shape==oalph)return a1*v;do{u=jStat._random_fn()}while(u===0);return Math.pow(u,1/oalph)*a1*v};(function(funcs){for(var i=0;i1||x<0)return 0;if(alpha==1&&beta==1)return 1;if(alpha<512&&beta<512){return Math.pow(x,alpha-1)*Math.pow(1-x,beta-1)/jStat.betafn(alpha,beta)}else{return Math.exp((alpha-1)*Math.log(x)+(beta-1)*Math.log(1-x)-jStat.betaln(alpha,beta))}},cdf:function cdf(x,alpha,beta){return x>1||x<0?(x>1)*1:jStat.ibeta(x,alpha,beta)},inv:function inv(x,alpha,beta){return jStat.ibetainv(x,alpha,beta)},mean:function mean(alpha,beta){return alpha/(alpha+beta)},median:function median(alpha,beta){return jStat.ibetainv(.5,alpha,beta)},mode:function mode(alpha,beta){return(alpha-1)/(alpha+beta-2)},sample:function sample(alpha,beta){var u=jStat.randg(alpha);return u/(u+jStat.randg(beta))},variance:function variance(alpha,beta){return alpha*beta/(Math.pow(alpha+beta,2)*(alpha+beta+1))}});jStat.extend(jStat.centralF,{pdf:function pdf(x,df1,df2){var p,q,f;if(x<0)return 0;if(df1<=2){if(x===0&&df1<2){return Infinity}if(x===0&&df1===2){return 1}return 1/jStat.betafn(df1/2,df2/2)*Math.pow(df1/df2,df1/2)*Math.pow(x,df1/2-1)*Math.pow(1+df1/df2*x,-(df1+df2)/2)}p=df1*x/(df2+x*df1);q=df2/(df2+x*df1);f=df1*q/2;return f*jStat.binomial.pdf((df1-2)/2,(df1+df2-2)/2,p)},cdf:function cdf(x,df1,df2){if(x<0)return 0;return jStat.ibeta(df1*x/(df1*x+df2),df1/2,df2/2)},inv:function inv(x,df1,df2){return df2/(df1*(1/jStat.ibetainv(x,df1/2,df2/2)-1))},mean:function mean(df1,df2){return df2>2?df2/(df2-2):undefined},mode:function mode(df1,df2){return df1>2?df2*(df1-2)/(df1*(df2+2)):undefined},sample:function sample(df1,df2){var x1=jStat.randg(df1/2)*2;var x2=jStat.randg(df2/2)*2;return x1/df1/(x2/df2)},variance:function variance(df1,df2){if(df2<=4)return undefined;return 2*df2*df2*(df1+df2-2)/(df1*(df2-2)*(df2-2)*(df2-4))}});jStat.extend(jStat.cauchy,{pdf:function pdf(x,local,scale){if(scale<0){return 0}return scale/(Math.pow(x-local,2)+Math.pow(scale,2))/Math.PI},cdf:function cdf(x,local,scale){return Math.atan((x-local)/scale)/Math.PI+.5},inv:function(p,local,scale){return local+scale*Math.tan(Math.PI*(p-.5))},median:function median(local){return local},mode:function mode(local){return local},sample:function sample(local,scale){return jStat.randn()*Math.sqrt(1/(2*jStat.randg(.5)))*scale+local}});jStat.extend(jStat.chisquare,{pdf:function pdf(x,dof){if(x<0)return 0;return x===0&&dof===2?.5:Math.exp((dof/2-1)*Math.log(x)-x/2-dof/2*Math.log(2)-jStat.gammaln(dof/2))},cdf:function cdf(x,dof){if(x<0)return 0;return jStat.lowRegGamma(dof/2,x/2)},inv:function(p,dof){return 2*jStat.gammapinv(p,.5*dof)},mean:function(dof){return dof},median:function median(dof){return dof*Math.pow(1-2/(9*dof),3)},mode:function mode(dof){return dof-2>0?dof-2:0},sample:function sample(dof){return jStat.randg(dof/2)*2},variance:function variance(dof){return 2*dof}});jStat.extend(jStat.exponential,{pdf:function pdf(x,rate){return x<0?0:rate*Math.exp(-rate*x)},cdf:function cdf(x,rate){return x<0?0:1-Math.exp(-rate*x)},inv:function(p,rate){return-Math.log(1-p)/rate},mean:function(rate){return 1/rate},median:function(rate){return 1/rate*Math.log(2)},mode:function mode(){return 0},sample:function sample(rate){return-1/rate*Math.log(jStat._random_fn())},variance:function(rate){return Math.pow(rate,-2)}});jStat.extend(jStat.gamma,{pdf:function pdf(x,shape,scale){if(x<0)return 0;return x===0&&shape===1?1/scale:Math.exp((shape-1)*Math.log(x)-x/scale-jStat.gammaln(shape)-shape*Math.log(scale))},cdf:function cdf(x,shape,scale){if(x<0)return 0;return jStat.lowRegGamma(shape,x/scale)},inv:function(p,shape,scale){return jStat.gammapinv(p,shape)*scale},mean:function(shape,scale){return shape*scale},mode:function mode(shape,scale){if(shape>1)return(shape-1)*scale;return undefined},sample:function sample(shape,scale){return jStat.randg(shape)*scale},variance:function variance(shape,scale){return shape*scale*scale}});jStat.extend(jStat.invgamma,{pdf:function pdf(x,shape,scale){if(x<=0)return 0;return Math.exp(-(shape+1)*Math.log(x)-scale/x-jStat.gammaln(shape)+shape*Math.log(scale))},cdf:function cdf(x,shape,scale){if(x<=0)return 0;return 1-jStat.lowRegGamma(shape,scale/x)},inv:function(p,shape,scale){return scale/jStat.gammapinv(1-p,shape)},mean:function(shape,scale){return shape>1?scale/(shape-1):undefined},mode:function mode(shape,scale){return scale/(shape+1)},sample:function sample(shape,scale){return scale/jStat.randg(shape)},variance:function variance(shape,scale){if(shape<=2)return undefined;return scale*scale/((shape-1)*(shape-1)*(shape-2))}});jStat.extend(jStat.kumaraswamy,{pdf:function pdf(x,alpha,beta){if(x===0&&alpha===1)return beta;else if(x===1&&beta===1)return alpha;return Math.exp(Math.log(alpha)+Math.log(beta)+(alpha-1)*Math.log(x)+(beta-1)*Math.log(1-Math.pow(x,alpha)))},cdf:function cdf(x,alpha,beta){if(x<0)return 0;else if(x>1)return 1;return 1-Math.pow(1-Math.pow(x,alpha),beta)},inv:function inv(p,alpha,beta){return Math.pow(1-Math.pow(1-p,1/beta),1/alpha)},mean:function(alpha,beta){return beta*jStat.gammafn(1+1/alpha)*jStat.gammafn(beta)/jStat.gammafn(1+1/alpha+beta)},median:function median(alpha,beta){return Math.pow(1-Math.pow(2,-1/beta),1/alpha)},mode:function mode(alpha,beta){if(!(alpha>=1&&beta>=1&&(alpha!==1&&beta!==1)))return undefined;return Math.pow((alpha-1)/(alpha*beta-1),1/alpha)},variance:function variance(){throw new Error("variance not yet implemented")}});jStat.extend(jStat.lognormal,{pdf:function pdf(x,mu,sigma){if(x<=0)return 0;return Math.exp(-Math.log(x)-.5*Math.log(2*Math.PI)-Math.log(sigma)-Math.pow(Math.log(x)-mu,2)/(2*sigma*sigma))},cdf:function cdf(x,mu,sigma){if(x<0)return 0;return.5+.5*jStat.erf((Math.log(x)-mu)/Math.sqrt(2*sigma*sigma))},inv:function(p,mu,sigma){return Math.exp(-1.4142135623730951*sigma*jStat.erfcinv(2*p)+mu)},mean:function mean(mu,sigma){return Math.exp(mu+sigma*sigma/2)},median:function median(mu){return Math.exp(mu)},mode:function mode(mu,sigma){return Math.exp(mu-sigma*sigma)},sample:function sample(mu,sigma){return Math.exp(jStat.randn()*sigma+mu)},variance:function variance(mu,sigma){return(Math.exp(sigma*sigma)-1)*Math.exp(2*mu+sigma*sigma)}});jStat.extend(jStat.noncentralt,{pdf:function pdf(x,dof,ncp){var tol=1e-14;if(Math.abs(ncp)tol||value>tol){lastvalue=value;if(j>0){p*=ncp*ncp/(2*j);q*=ncp*ncp/(2*(j+1/2))}value=p*jStat.beta.cdf(y,j+.5,dof/2)+q*jStat.beta.cdf(y,j+1,dof/2);prob+=.5*value;j++}return flip?1-prob:prob}});jStat.extend(jStat.normal,{pdf:function pdf(x,mean,std){return Math.exp(-.5*Math.log(2*Math.PI)-Math.log(std)-Math.pow(x-mean,2)/(2*std*std))},cdf:function cdf(x,mean,std){return.5*(1+jStat.erf((x-mean)/Math.sqrt(2*std*std)))},inv:function(p,mean,std){return-1.4142135623730951*std*jStat.erfcinv(2*p)+mean},mean:function(mean){return mean},median:function median(mean){return mean},mode:function(mean){return mean},sample:function sample(mean,std){return jStat.randn()*std+mean},variance:function(mean,std){return std*std}});jStat.extend(jStat.pareto,{pdf:function pdf(x,scale,shape){if(x1e100?1e100:dof;return 1/(Math.sqrt(dof)*jStat.betafn(.5,dof/2))*Math.pow(1+x*x/dof,-((dof+1)/2))},cdf:function cdf(x,dof){var dof2=dof/2;return jStat.ibeta((x+Math.sqrt(x*x+dof))/(2*Math.sqrt(x*x+dof)),dof2,dof2)},inv:function(p,dof){var x=jStat.ibetainv(2*Math.min(p,1-p),.5*dof,.5);x=Math.sqrt(dof*(1-x)/x);return p>.5?x:-x},mean:function mean(dof){return dof>1?0:undefined},median:function median(){return 0},mode:function mode(){return 0},sample:function sample(dof){return jStat.randn()*Math.sqrt(dof/(2*jStat.randg(dof/2)))},variance:function variance(dof){return dof>2?dof/(dof-2):dof>1?Infinity:undefined}});jStat.extend(jStat.weibull,{pdf:function pdf(x,scale,shape){if(x<0||scale<0||shape<0)return 0;return shape/scale*Math.pow(x/scale,shape-1)*Math.exp(-Math.pow(x/scale,shape))},cdf:function cdf(x,scale,shape){return x<0?0:1-Math.exp(-Math.pow(x/scale,shape))},inv:function(p,scale,shape){return scale*Math.pow(-Math.log(1-p),1/shape)},mean:function(scale,shape){return scale*jStat.gammafn(1+1/shape)},median:function median(scale,shape){return scale*Math.pow(Math.log(2),1/shape)},mode:function mode(scale,shape){if(shape<=1)return 0;return scale*Math.pow((shape-1)/shape,1/shape)},sample:function sample(scale,shape){return scale*Math.pow(-Math.log(jStat._random_fn()),1/shape)},variance:function variance(scale,shape){return scale*scale*jStat.gammafn(1+2/shape)-Math.pow(jStat.weibull.mean(scale,shape),2)}});jStat.extend(jStat.uniform,{pdf:function pdf(x,a,b){return xb?0:1/(b-a)},cdf:function cdf(x,a,b){if(xeps){a2=a1;c9=-(a+m9)*(a+b+m9)*x/(a+2*m9)/(a+2*m9+1);a0=a1+c9*a0;b0=b1+c9*b0;m9=m9+1;c9=m9*(b-m9)*x/(a+2*m9-1)/(a+2*m9);a1=a0+c9*a1;b1=b0+c9*b1;a0=a0/b1;b0=b0/b1;a1=a1/b1;b1=1}return a1/a}jStat.extend(jStat.binomial,{pdf:function pdf(k,n,p){return p===0||p===1?n*p===k?1:0:jStat.combination(n,k)*Math.pow(p,k)*Math.pow(1-p,n-k)},cdf:function cdf(x,n,p){var betacdf;var eps=1e-10;if(x<0)return 0;if(x>=n)return 1;if(p<0||p>1||n<=0)return NaN;x=Math.floor(x);var z=p;var a=x+1;var b=n-x;var s=a+b;var bt=Math.exp(jStat.gammaln(s)-jStat.gammaln(b)-jStat.gammaln(a)+a*Math.log(z)+b*Math.log(1-z));if(z<(a+1)/(s+2))betacdf=bt*betinc(z,a,b,eps);else betacdf=1-bt*betinc(1-z,b,a,eps);return Math.round((1-betacdf)*(1/eps))/(1/eps)}});jStat.extend(jStat.negbin,{pdf:function pdf(k,r,p){if(k!==k>>>0)return false;if(k<0)return 0;return jStat.combination(k+r-1,r-1)*Math.pow(1-p,k)*Math.pow(p,r)},cdf:function cdf(x,r,p){var sum=0,k=0;if(x<0)return 0;for(;k<=x;k++){sum+=jStat.negbin.pdf(k,r,p)}return sum}});jStat.extend(jStat.hypgeom,{pdf:function pdf(k,N,m,n){if(k!==k|0){return false}else if(k<0||kn||k>m){return 0}else if(m*2>N){if(n*2>N){return jStat.hypgeom.pdf(N-m-n+k,N,N-m,N-n)}else{return jStat.hypgeom.pdf(n-k,N,N-m,n)}}else if(n*2>N){return jStat.hypgeom.pdf(m-k,N,m,N-n)}else if(m1&&samplesDone=n||x>=m){return 1}else if(m*2>N){if(n*2>N){return jStat.hypgeom.cdf(N-m-n+x,N,N-m,N-n)}else{return 1-jStat.hypgeom.cdf(n-x-1,N,N-m,n)}}else if(n*2>N){return 1-jStat.hypgeom.cdf(m-x-1,N,m,N-n)}else if(m1&&samplesDoneL);return k-1},sampleLarge:function sampleLarge(l){var lam=l;var k;var U,V,slam,loglam,a,b,invalpha,vr,us;slam=Math.sqrt(lam);loglam=Math.log(lam);b=.931+2.53*slam;a=-.059+.02483*b;invalpha=1.1239+1.1328/(b-3.4);vr=.9277-3.6224/(b-2);while(1){U=Math.random()-.5;V=Math.random();us=.5-Math.abs(U);k=Math.floor((2*a/us+b)*U+lam+.43);if(us>=.07&&V<=vr){return k}if(k<0||us<.013&&V>us){continue}if(Math.log(V)+Math.log(invalpha)-Math.log(a/(us*us)+b)<=-lam+k*loglam-jStat.loggam(k+1)){return k}}},sample:function sample(l){if(l<10)return this.sampleSmall(l);else return this.sampleLarge(l)}});jStat.extend(jStat.triangular,{pdf:function pdf(x,a,b,c){if(b<=a||cb){return NaN}else{if(xb){return 0}else if(xb)return NaN;if(x<=a)return 0;else if(x>=b)return 1;if(x<=c)return Math.pow(x-a,2)/((b-a)*(c-a));else return 1-Math.pow(b-x,2)/((b-a)*(b-c))},inv:function inv(p,a,b,c){if(b<=a||cb){return NaN}else{if(p<=(c-a)/(b-a)){return a+(b-a)*Math.sqrt(p*((c-a)/(b-a)))}else{return a+(b-a)*(1-Math.sqrt((1-p)*(1-(c-a)/(b-a))))}}},mean:function mean(a,b,c){return(a+b+c)/3},median:function median(a,b,c){if(c<=(a+b)/2){return b-Math.sqrt((b-a)*(b-c))/Math.sqrt(2)}else if(c>(a+b)/2){return a+Math.sqrt((b-a)*(c-a))/Math.sqrt(2)}},mode:function mode(a,b,c){return c},sample:function sample(a,b,c){var u=jStat._random_fn();if(u<(c-a)/(b-a))return a+Math.sqrt(u*(b-a)*(c-a));return b-Math.sqrt((1-u)*(b-a)*(b-c))},variance:function variance(a,b,c){return(a*a+b*b+c*c-a*b-a*c-b*c)/18}});jStat.extend(jStat.arcsine,{pdf:function pdf(x,a,b){if(b<=a)return NaN;return x<=a||x>=b?0:2/Math.PI*Math.pow(Math.pow(b-a,2)-Math.pow(2*x-a-b,2),-.5)},cdf:function cdf(x,a,b){if(x=bb)return 1;var pr_w=2*jStat.normal.cdf(qsqz,0,1,1,0)-1;if(pr_w>=Math.exp(C2/cc))pr_w=Math.pow(pr_w,cc);else pr_w=0;var wincr;if(w>wlar)wincr=wincr1;else wincr=wincr2;var blb=qsqz;var binc=(bb-qsqz)/wincr;var bub=blb+binc;var einsum=0;var cc1=cc-1;for(var wi=1;wi<=wincr;wi++){var elsum=0;var a=.5*(bub+blb);var b=.5*(bub-blb);for(var jj=1;jj<=nleg;jj++){var j,xx;if(ihalfC3)break;var pplus=2*jStat.normal.cdf(ac,0,1,1,0);var pminus=2*jStat.normal.cdf(ac,w,1,1,0);var rinsum=pplus*.5-pminus*.5;if(rinsum>=Math.exp(C1/cc1)){rinsum=aleg[j-1]*Math.exp(-(.5*qexpo))*Math.pow(rinsum,cc1);elsum+=rinsum}}elsum*=2*b*cc/Math.sqrt(2*Math.PI);einsum+=elsum;blb=bub;bub+=binc}pr_w+=einsum;if(pr_w<=Math.exp(C1/rr))return 0;pr_w=Math.pow(pr_w,rr);if(pr_w>=1)return 1;return pr_w}function tukeyQinv(p,c,v){var p0=.322232421088;var q0=.099348462606;var p1=-1;var q1=.588581570495;var p2=-.342242088547;var q2=.531103462366;var p3=-.204231210125;var q3=.10353775285;var p4=-453642210148e-16;var q4=.0038560700634;var c1=.8832;var c2=.2368;var c3=1.214;var c4=1.208;var c5=1.4142;var vmax=120;var ps=.5-.5*p;var yi=Math.sqrt(Math.log(1/(ps*ps)));var t=yi+((((yi*p4+p3)*yi+p2)*yi+p1)*yi+p0)/((((yi*q4+q3)*yi+q2)*yi+q1)*yi+q0);if(vdlarg)return tukeyWprob(q,rr,cc);var f2=df*.5;var f2lf=f2*Math.log(df)-df*Math.log(2)-jStat.gammaln(f2);var f21=f2-1;var ff4=df*.25;var ulen;if(df<=dhaf)ulen=ulen1;else if(df<=dquar)ulen=ulen2;else if(df<=deigh)ulen=ulen3;else ulen=ulen4;f2lf+=Math.log(ulen);var ans=0;for(var i=1;i<=50;i++){var otsum=0;var twa1=(2*i-1)*ulen;for(var jj=1;jj<=nlegq;jj++){var j,t1;if(ihalfq=eps1){if(ihalfq=1&&otsum<=eps2)break;ans+=otsum}if(otsum>eps2){throw new Error("tukey.cdf failed to converge")}if(ans>1)ans=1;return ans},inv:function(p,nmeans,df){var rr=1;var cc=nmeans;var eps=1e-4;var maxiter=50;if(df<2||rr<1||cc<2)return NaN;if(p<0||p>1)return NaN;if(p===0)return 0;if(p===1)return Infinity;var x0=tukeyQinv(p,cc,df);var valx0=jStat.tukey.cdf(x0,nmeans,df)-p;var x1;if(valx0>0)x1=Math.max(0,x0-1);else x1=x0+1;var valx1=jStat.tukey.cdf(x1,nmeans,df)-p;var ans;for(var iter=1;iteri){submatrix[row-1][col-1]=a[row][col]}}}var sign=i%2?-1:1;determinant+=det(submatrix)*a[0][i]*sign}return determinant},gauss_elimination:function gauss_elimination(a,b){var i=0,j=0,n=a.length,m=a[0].length,factor=1,sum=0,x=[],maug,pivot,temp,k;a=jStat.aug(a,b);maug=a[0].length;for(i=0;i=0;i--){sum=0;for(j=i+1;j<=n-1;j++){sum=sum+x[j]*a[i][j]}x[i]=(a[i][maug-1]-sum)/a[i][i]}return x},gauss_jordan:function gauss_jordan(a,b){var m=jStat.aug(a,b);var h=m.length;var w=m[0].length;var c=0;var x,y,y2;for(y=0;yMath.abs(m[maxrow][y]))maxrow=y2}var tmp=m[y];m[y]=m[maxrow];m[maxrow]=tmp;for(y2=y+1;y2=0;y--){c=m[y][y];for(y2=0;y2y-1;x--){m[y2][x]-=m[y][x]*m[y2][y]/c}}m[y][y]/=c;for(x=h;xj){l[i][j]=a[i][j];u[i][j]=d[i][j]=0}else if(ir){xv=xk;xk=jStat.add(jStat.multiply(h,xv),c);i++}return xk},gauss_seidel:function gauss_seidel(a,b,x,r){var i=0;var n=a.length;var l=[];var u=[];var d=[];var j,xv,c,h,xk;for(;ij){l[i][j]=a[i][j];u[i][j]=d[i][j]=0}else if(ir){xv=xk;xk=jStat.add(jStat.multiply(h,xv),c);i=i+1}return xk},SOR:function SOR(a,b,x,r,w){var i=0;var n=a.length;var l=[];var u=[];var d=[];var j,xv,c,h,xk;for(;ij){l[i][j]=a[i][j];u[i][j]=d[i][j]=0}else if(ir){xv=xk;xk=jStat.add(jStat.multiply(h,xv),c);i++}return xk},householder:function householder(a){var m=a.length;var n=a[0].length;var i=0;var w=[];var p=[];var alpha,r,k,j,factor;for(;i0?-1:1;alpha=factor*Math.sqrt(alpha);r=Math.sqrt((alpha*alpha-a[i+1][i]*alpha)/2);w=jStat.zeros(m,1);w[i+1][0]=(a[i+1][i]-alpha)/(2*r);for(k=i+2;k0?Math.PI/4:-Math.PI/4;else theta=Math.atan(2*a[p][q]/(a[p][p]-a[q][q]))/2;s=jStat.identity(n,n);s[p][p]=Math.cos(theta);s[p][q]=-Math.sin(theta);s[q][p]=Math.sin(theta);s[q][q]=Math.cos(theta);e=jStat.multiply(e,s);b=jStat.multiply(jStat.multiply(jStat.inv(s),a),s);a=b;condition=0;for(i=1;i.001){condition=1}}}}for(i=0;i=h_min){y1=pos(X,x+h);y2=pos(X,x);g[i]=(f[y1]-2*f[y2]+f[2*y2-y1])/(h*h);h/=2;i++}a=g.length;m=1;while(a!=1){for(j=0;jvalue)break}j-=1;return F[j]+(value-X[j])*b[j]+jStat.sq(value-X[j])*c[j]+(value-X[j])*jStat.sq(value-X[j])*d[j]},gauss_quadrature:function gauss_quadrature(){throw new Error("gauss_quadrature not yet implemented")},PCA:function PCA(X){var m=X.length;var n=X[0].length;var i=0;var j,temp1;var u=[];var D=[];var result=[];var temp2=[];var Y=[];var Bt=[];var B=[];var C=[];var V=[];var Vt=[];for(i=0;i2){z=jStat.zscore(args[0],args[1],args[2]);return args[3]===1?jStat.normal.cdf(-Math.abs(z),0,1):jStat.normal.cdf(-Math.abs(z),0,1)*2}else{z=args[0];return args[1]===1?jStat.normal.cdf(-Math.abs(z),0,1):jStat.normal.cdf(-Math.abs(z),0,1)*2}}}});jStat.extend(jStat.fn,{zscore:function zscore(value,flag){return(value-this.mean())/this.stdev(flag)},ztest:function ztest(value,sides,flag){var zscore=Math.abs(this.zscore(value,flag));return sides===1?jStat.normal.cdf(-zscore,0,1):jStat.normal.cdf(-zscore,0,1)*2}});jStat.extend({tscore:function tscore(){var args=slice.call(arguments);return args.length===4?(args[0]-args[1])/(args[2]/Math.sqrt(args[3])):(args[0]-jStat.mean(args[1]))/(jStat.stdev(args[1],true)/Math.sqrt(args[1].length))},ttest:function ttest(){var args=slice.call(arguments);var tscore;if(args.length===5){tscore=Math.abs(jStat.tscore(args[0],args[1],args[2],args[3]));return args[4]===1?jStat.studentt.cdf(-tscore,args[3]-1):jStat.studentt.cdf(-tscore,args[3]-1)*2}if(isNumber(args[1])){tscore=Math.abs(args[0]);return args[2]==1?jStat.studentt.cdf(-tscore,args[1]-1):jStat.studentt.cdf(-tscore,args[1]-1)*2}tscore=Math.abs(jStat.tscore(args[0],args[1]));return args[2]==1?jStat.studentt.cdf(-tscore,args[1].length-1):jStat.studentt.cdf(-tscore,args[1].length-1)*2}});jStat.extend(jStat.fn,{tscore:function tscore(value){return(value-this.mean())/(this.stdev(true)/Math.sqrt(this.cols()))},ttest:function ttest(value,sides){return sides===1?1-jStat.studentt.cdf(Math.abs(this.tscore(value)),this.cols()-1):jStat.studentt.cdf(-Math.abs(this.tscore(value)),this.cols()-1)*2}});jStat.extend({anovafscore:function anovafscore(){var args=slice.call(arguments),expVar,sample,sampMean,sampSampMean,tmpargs,unexpVar,i,j;if(args.length===1){tmpargs=new Array(args[0].length);for(i=0;i1||p2>1||p1<=0||p2<=0){throw new Error("Proportions should be greater than 0 and less than 1")}var pooled=(p1*n1+p2*n2)/(n1+n2);var se=Math.sqrt(pooled*(1-pooled)*(1/n1+1/n2));return(p1-p2)/se}jStat.extend(jStat.fn,{oneSidedDifferenceOfProportions:function oneSidedDifferenceOfProportions(p1,n1,p2,n2){var z=differenceOfProportions(p1,n1,p2,n2);return jStat.ztest(z,1)},twoSidedDifferenceOfProportions:function twoSidedDifferenceOfProportions(p1,n1,p2,n2){var z=differenceOfProportions(p1,n1,p2,n2);return jStat.ztest(z,2)}})})(jStat,Math);jStat.models=function(){function sub_regress(exog){var var_count=exog[0].length;var modelList=jStat.arange(var_count).map(function(endog_index){var exog_index=jStat.arange(var_count).filter(function(i){return i!==endog_index});return ols(jStat.col(exog,endog_index).map(function(x){return x[0]}),jStat.col(exog,exog_index))});return modelList}function ols(endog,exog){var nobs=endog.length;var df_model=exog[0].length-1;var df_resid=nobs-df_model-1;var coef=jStat.lstsq(exog,endog);var predict=jStat.multiply(exog,coef.map(function(x){return[x]})).map(function(p){return p[0]});var resid=jStat.subtract(endog,predict);var ybar=jStat.mean(endog);var SSE=jStat.sum(predict.map(function(f){return Math.pow(f-ybar,2)}));var SSR=jStat.sum(endog.map(function(y,i){return Math.pow(y-predict[i],2)}));var SST=SSE+SSR;var R2=SSE/SST;return{exog:exog,endog:endog,nobs:nobs,df_model:df_model,df_resid:df_resid,coef:coef,predict:predict,resid:resid,ybar:ybar,SST:SST,SSE:SSE,SSR:SSR,R2:R2}}function t_test(model){var subModelList=sub_regress(model.exog);var sigmaHat=Math.sqrt(model.SSR/model.df_resid);var seBetaHat=subModelList.map(function(mod){var SST=mod.SST;var R2=mod.R2;return sigmaHat/Math.sqrt(SST*(1-R2))});var tStatistic=model.coef.map(function(coef,i){return(coef-0)/seBetaHat[i]});var pValue=tStatistic.map(function(t){var leftppf=jStat.studentt.cdf(t,model.df_resid);return(leftppf>.5?1-leftppf:leftppf)*2});var c=jStat.studentt.inv(.975,model.df_resid);var interval95=model.coef.map(function(coef,i){var d=c*seBetaHat[i];return[coef-d,coef+d]});return{se:seBetaHat,t:tStatistic,p:pValue,sigmaHat:sigmaHat,interval95:interval95}}function F_test(model){var F_statistic=model.R2/model.df_model/((1-model.R2)/model.df_resid);var fcdf=function(x,n1,n2){return jStat.beta.cdf(x/(n2/n1+x),n1/2,n2/2)};var pvalue=1-fcdf(F_statistic,model.df_model,model.df_resid);return{F_statistic:F_statistic,pvalue:pvalue}}function ols_wrap(endog,exog){var model=ols(endog,exog);var ttest=t_test(model);var ftest=F_test(model);var adjust_R2=1-(1-model.R2)*((model.nobs-1)/model.df_resid);model.t=ttest;model.f=ftest;model.adjust_R2=adjust_R2;return model}return{ols:ols_wrap}}();jStat.extend({buildxmatrix:function buildxmatrix(){var matrixRows=new Array(arguments.length);for(var i=0;i1){result=[];for(i=0;i theParams.value ? ">" : "<"); + } + + const N = this.results.N; + + const out = `
N = ${N}
That's all we know!
`; + return out; + } + + makeTestDescription(iTestID, includeName) { + return `this is a default description for a test (${iTestID})`; + } + + + /** + * Compute array of compatible test IDs given `data.xAttData` and `data.yAttData`. (e.g., NN01...). + * This depends only on their existence and variable type (numeric or categorical) + * @returns {[]} Array of test configuration IDs + */ + static findCompatibleTestConfigurations() { + + const X = data.xAttData; + let out = []; + + if (X) { + const Y = data.yAttData; + + const xType = X && testimate.state.dataTypes[X.name]; + const yType = Y && testimate.state.dataTypes[Y.name]; + + console.log(`finding tests for ${X && X.name} (${xType}) vs ${Y && Y.name} (${yType}) `); + let passed = ""; + + const pairable = X && Y && X.theRawArray && + Y.theRawArray && + (data.xAttData.theRawArray.length === data.yAttData.theRawArray.length); + + // loop over all possible test configurations, identified by `id` + for (let id in Test.configs) { + let match = true; + const theConfig = Test.configs[id]; // theConfig for THIS test configuration + + if (theConfig.paired && !pairable) match = false; + + if (theConfig.yType && !Y) match = false; // the config demands a y-type, but there is no Y. + + if (theConfig.xType === 'binary' && !X.isBinary()) match = false; + if (Y && theConfig.yType === 'binary' && !Y.isBinary()) match = false; + if (theConfig.xType === 'numeric' && !X.isNumeric()) match = false; + if (Y && theConfig.yType === 'numeric' && !Y.isNumeric()) match = false; + if (theConfig.xType === 'categorical' && !X.isCategorical()) match = false; + if (Y && theConfig.yType === 'categorical' && !Y.isCategorical()) match = false; + + if (match) { + out.push(theConfig.id); + } + } + } + console.log(` ... compatible tests: ${out.join(", ")}`); + return out; + }; + + /** + * Make a text description of the test configuration. + * The basic structure is something like, + * "Compare mean(post) to mean(pre)" + * + * @param iTest The ID of the test we're doing. + * @param iIncludeName Precede that with the name? (Boolean), e.g., "Two-sample t. + * @returns {string} + */ + static makeTestDescription( ) { + const theID = testimate.state.testID; + const theName = Test.configs[theID].name; + return `default description for ${theName} (${theID})`; + } + + static makeMenuString(iiD) { + + const theID = iiD ? iiD : testimate.state.testID; + const theName = Test.configs[theID].name; + return `placeholder menu string for ${theName} (${theID})`; + } + + makeConfigureGuts() { + return `configuration: this string should never appear!`; + } + + /** + * Splits the first argument (an Array) into two arrays depending on the values in the second. + * + * @param iData + * @param iGroups + * @param iLabel the value of "iGroups" that goes into the first output array + * + * */ + static splitByGroup(iData, iGroups, iLabel) { + let A = []; + let B = []; + + for (let i = 0; i < iData.length; i++ ) { + if (iGroups[i] === iLabel) { + A.push(iData[i]); + } else { + B.push(iData[i]); + } + } + return [A, B]; + } + + static getComplementaryValue(iAttData, iValue) { + + const out = iAttData.isBinary() ? + handlers.nextValueInList([...iAttData.valueSet], iValue) : // the OTHER value, e.g., "Female" + `${localize.getString('notP', iValue)}`; // e.g., "not Male" + + return out; + } + + /** + * configurations for all possible tests + * @type {{B_02: {xType: string, yType: null, emitted: string, name: string, id: string, paired: boolean}, B_01: {xType: string, yType: null, emitted: string, name: string, id: string, paired: boolean}, C_01: {xType: string, yType: null, emitted: string, name: string, id: string, paired: boolean}, NN01: {xType: string, yType: string, emitted: string, testing: string, name: string, id: string, paired: boolean}, NN02: {xType: string, yType: string, emitted: string, testing: string, name: string, id: string, paired: boolean}, NN03: {xType: string, yType: string, emitted: string, testing: string, name: string, id: string, paired: boolean}, NB01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, NC01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, BN01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, CN01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, N_01: {xType: string, yType: null, emitted: string, testing: string, name: string, id: string, paired: boolean}, BB02: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, BC01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, BB03: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, CC01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, CB01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}, BB01: {xType: string, yType: string, emitted: string, name: string, id: string, paired: boolean}}} + */ + static configs = { + N_01: { + id: `N_01`, + name: 'one-sample t', + xType: 'numeric', + yType: null, + paired: false, + groupAxis : "", + emitted: `P,mean,sign,value,SE,t,tCrit,N,conf,CImin,CImax,df`, + testing: `mean`, + paramExceptions: {}, + makeMenuString: ( ) => {return OneSampleT.makeMenuString(`N_01`);}, + fresh: (ix) => { + return new OneSampleT(ix) + }, + + }, + NN01: { + id: `NN01`, + name: 'paired t', + xType: 'numeric', + yType: 'numeric', + paired: true, + groupAxis : "", + emitted: `P,mean,sign,value,SE,t,tCrit,N,conf,CImin,CImax,df`, + testing: `mean`, + paramExceptions: {}, + makeMenuString: ( ) => {return Paired.makeMenuString(`NN01`);}, + fresh: (ix) => { return new Paired(ix) }, + }, + NN02: { // difference of means, X vs Y + id: `NN02`, + name: 'two-sample t', + xType: 'numeric', + yType: 'numeric', + paired: false, + groupAxis : "", + emitted: `P,mean1,mean2,diff,sign,value,t,tCrit,N,conf,CImin,CImax,df`, + testing: `diff`, + paramExceptions: {}, + makeMenuString: ( ) => {return TwoSampleT.makeMenuString(`NN02`);}, + fresh: (ix) => { return new TwoSampleT(ix, false) }, + }, + NN03: { + id: `NN03`, + name: 'linear regression', + xType: 'numeric', + yType: 'numeric', + paired: true, + groupAxis : "", + emitted: `P,slope,sign,value,intercept,rho,rsq,t,tCrit,N,conf,slopeCImin,slopeCImax,df`, + paramExceptions: {}, + makeMenuString: ( ) => {return Regression.makeMenuString(`NN03`);}, + fresh: (ix) => { return new Regression(ix) }, + testing: `slope`, + }, + NN04: { + id: `NN04`, + name: 'correlation', + xType: 'numeric', + yType: 'numeric', + paired: true, + groupAxis : "", + emitted: `P,rho,sign,value,rsq,t,tCrit,N,conf,CImin,CImax,df`, + paramExceptions: {}, + makeMenuString: ( ) => {return Correlation.makeMenuString(`NN04`);}, + fresh: (ix) => { return new Correlation(ix) }, + testing: `slope`, + }, + NB01: { + id: `NB01`, + name: `two-sample t`, + xType: 'numeric', + yType: 'binary', + paired: true, + groupAxis : "", + emitted: `P,mean1,mean2,diff,sign,value,t,tCrit,N,conf,CImin,CImax,df`, + testing : 'diff', + paramExceptions: {}, + makeMenuString: ( ) => {return TwoSampleT.makeMenuString(`NB01`);}, + fresh: (ix) => { return new TwoSampleT(ix, true) }, + }, + B_01: { + id: `B_01`, + name: `test proportion`, + xType: 'binary', + yType: null, + paired: false, + groupAxis : "", + emitted: `P,prop,sign,value,SE,z,zCrit,N,conf,CImin,CImax`, + testing : 'prop', + paramExceptions: { value : 0.5}, + makeMenuString: () => {return OneSampleP.makeMenuString();}, + fresh: (ix ) => {return new OneSampleP(ix)}, + }, + BB01: { // compare props using split + id: `BB01`, + name: `compare proportions (grouped)`, + xType: 'binary', + yType: `binary`, + paired: true, + groupAxis : "", + emitted: `P,prop1,prop2,pDiff,sign,value,N,N1,N2,z,zCrit,conf,CImin,CImax`, + paramExceptions: {}, + makeMenuString: ( ) => {return TwoSampleP.makeMenuString(`BB01`);}, + fresh: (ix) => { return new TwoSampleP(ix, true) }, + }, + BB02: { // two-sample compare props + id: `BB02`, + name: `compare proportions (two sample)`, + xType: 'binary', + yType: `binary`, + paired: false, + groupAxis : "", + emitted: `P,prop1,prop2,pDiff,sign,value,N,N1,N2,z,zCrit,conf,CImin,CImax`, + paramExceptions: {}, + makeMenuString: ( ) => {return TwoSampleP.makeMenuString(`BB02`);}, + fresh: (ix) => { return new TwoSampleP(ix, false) }, + }, +/* + B_02: { + id: `B_02`, + name: `goodness of fit`, + xType: 'binary', + yType: null, + paired: false, + groupAxis : "", + emitted: `N,P,chisq,df,chisqCrit,alpha`, + paramExceptions: {}, + makeMenuString: ( ) => {return Goodness.makeMenuString(`B_02`);}, + fresh: (ix) => { return new Goodness(ix) }, + }, +*/ + C_01: { + id: `C_01`, + name: `goodness of fit`, + xType: 'categorical', + yType: null, + paired: false, + groupAxis : "", + emitted: `P,chisq,chisqCrit,N,df,alpha,sides`, + paramExceptions: { sides : 1 , groupProportions : {} }, + makeMenuString: ( ) => {return Goodness.makeMenuString(`C_01`);}, + fresh: (ix) => { return new Goodness(ix) }, + }, + CC01: { + id: `CC01`, + name: `independence`, + xType: 'categorical', + yType: `categorical`, + paired: true, + groupAxis : "", + emitted: `P,chisq,chisqCrit,N,df,alpha,sides`, + paramExceptions: { sides : 1 }, + makeMenuString: ( ) => {return Independence.makeMenuString(`CC01`);}, + fresh: (ix) => { return new Independence(ix) }, + }, +/* CB01: { + id: `CB01`, + name: `independence`, + xType: 'categorical', + yType: `binary`, + paired: true, + groupAxis : "", + emitted: `N,P`, + paramExceptions: {}, + makeMenuString: ( ) => {return Test.makeMenuString(`CB01`);}, + fresh: (ix) => { return new Test(ix) }, + }, + BC01: { + id: `BC01`, + name: `independence`, + xType: 'binary', + yType: `categorical`, + paired: true, + groupAxis : "", + emitted: `N,P`, + paramExceptions: {}, + makeMenuString: ( ) => {return Test.makeMenuString(`BC01`);}, + fresh: (ix) => { return new Test(ix) }, + }, + BB03: { + id: `BB03`, + name: `independence`, + xType: 'binary', + yType: `binary`, + paired: true, + groupAxis : "", + emitted: `N,P`, + paramExceptions: {}, + makeMenuString: ( ) => {return Test.makeMenuString(`BB03`);}, + fresh: (ix) => { return new Test(ix) }, + },*/ + NC01: { + id: `NC01`, + name: `ANOVA`, + xType: 'numeric', + yType: 'categorical', + paired: true, + groupAxis : "", + emitted: `P,F,FCrit,SSR,SSE,SST,N,dfTreatment,dfError`, + paramExceptions: {}, + makeMenuString: ( ) => {return ANOVA.makeMenuString(`NC01`);}, + fresh: (ix) => { return new ANOVA(ix) }, + }, + BN01: { + id: `BN01`, + name: `logistic regression`, + xType: 'binary', + yType: `numeric`, + paired: true, + groupAxis : "X", + emitted: `slope,pos,cost,rate,N,iterations`, + paramExceptions: {}, + makeMenuString: ( ) => {return Logistic.makeMenuString(`BN01`);}, + fresh: (ix) => { return new Logistic(ix) }, + }, + CN01: { + id: `CN01`, + name: `logistic regression`, + xType: 'categorical', + yType: `numeric`, + paired: true, + groupAxis : "X", + emitted: `LSlope,pos,cost,rate,N,iterations`, + paramExceptions: {}, + makeMenuString: ( ) => {return Logistic.makeMenuString(`CN01`);}, + fresh: (ix) => { return new Logistic(ix) }, + }, + }; + +} + diff --git a/eepsmedia/plugins/testimate/src/connect.js b/eepsmedia/plugins/testimate/src/connect.js new file mode 100644 index 00000000..db1ad85c --- /dev/null +++ b/eepsmedia/plugins/testimate/src/connect.js @@ -0,0 +1,541 @@ +let connect; + +/** + * Singleton that communicates with CODAP + */ + + +connect = { + + caseChangeSubscriberIndex: null, + attributeChangeSubscriberIndex: null, + attributeDragDropSubscriberIndex: null, + mostRecentEmittedTest: null, + + initialize: async function () { + + // note: these subscriptions must happen BEFORE `.init` so that the `.on` there does not + // override our handlers. + codapInterface.on('update', 'interactiveState', "", handlers.restorePluginFromStore); + codapInterface.on('get', 'interactiveState', "", handlers.getPluginState); + + await codapInterface.init(this.iFrameDescriptor, handlers.restorePluginFromStore); + await this.registerForDragDropEvents(); + await this.allowReorg(); + + }, + + /** + * called from data.retrieveAllItemsFromCODAP + * + * @returns {Promise} + */ + getAllItems: async function () { + + let out = null; + const theMessage = { + "action": "get", + "resource": `dataContext[${testimate.state.dataset.name}].itemSearch[*]` + } + + const result = await codapInterface.sendRequest(theMessage); + if (result.success) { + data.dirtyData = false; + data.secondaryCleanupNeeded = true; + out = result.values; // array of objects, one of whose items is another "values" + } else { + alert(`Big trouble getting data!`) + } + return out; + }, + + /** + * Use the API to retrieve the dataset (data context) info for the named dataset + * Called by `data.` every time. + * @param iName + */ + getSourceDatasetInfo: async function (iName) { + let out = null; + + const tMessage = { + action: "get", + resource: `dataContext[${iName}]` + } + let result; + + try { + result = await codapInterface.sendRequest(tMessage); + if (result.success) { + out = result.values; // includes attributes but no cases + console.log(` * got dataset info`); + } else { + console.log(`Failure getting source dataset info`); + } + } catch (msg) { + console.log(`Trouble getting source dataset info: ${msg}`); + } + + return out; + }, + + /** + * Constant descriptor for the iFrame. + * Find and edit the values in `scrambler.constants` + */ + iFrameDescriptor: { + name: testimate.constants.pluginName, + title: testimate.constants.pluginName, + version: testimate.constants.version, + dimensions: testimate.constants.dimensions, // dimensions, + }, + + registerForAttributeEvents: function (iDatasetName) { + const sResource = `dataContext[${iDatasetName}].attribute`; + + if (this.attributeChangeSubscriberIndex) { // zero is a valid index... :P but it should be the "get" + codapInterface.off(this.attributeChangeSubscriberIndex); // blank that subscription. + } + + try { + this.attributeChangeSubscriberIndex = codapInterface.on( + 'notify', + sResource, + data.handleAttributeChangeNotice + ); + console.log(`registered for attribute changes in ${iDatasetName}. Index ${this.attributeChangeSubscriberIndex}`); + } catch (msg) { + console.log(`problem registering for attribute changes: ${msg}`); + } + }, + + /** + * Register for the dragDrop[attribute] event. + * + * Called from connect.initialize(); + */ + registerForDragDropEvents: function () { + const tResource = `dragDrop[attribute]`; + + this.attributeDragDropSubscriberIndex = codapInterface.on( + 'notify', tResource, testimate.dropManager.handleDragDrop + ) + console.log(`registered for drags and drops. Index ${this.attributeDragDropSubscriberIndex}`); + + }, + + /** + * register to receive notifications about changes to the data context (including selection) + * called from testimate.setDataset() + */ + registerForCaseChanges: async function (iName) { + if (this.caseChangeSubscriberIndex) { // zero is a valid index... :P but it should be the "get" + codapInterface.off(this.caseChangeSubscriberIndex); // blank that subscription. + } + + const sResource = `dataContextChangeNotice[${iName}]`; + // const sResource = `dataContext[${iName}].case`; + try { + this.caseChangeSubscriberIndex = codapInterface.on( + 'notify', + sResource, + data.handleCaseChangeNotice + ); + console.log(`registered for case changes in ${iName}. Index ${this.caseChangeSubscriberIndex}`); + } catch (msg) { + console.log(`problem registering for case changes: ${msg}`); + } + + }, + + rerandomizeSource: async function (iDatasetName) { + const theMessage = { + "action": "update", + "resource": `dataContext[${iDatasetName}]`, + "values": { + "rerandomize": true + } + } + + try { + const result = await codapInterface.sendRequest(theMessage); + } catch (msg) { + alert(`problem rerandomizing dataset: ${iDatasetName} : ${msg}`); + } + }, + + + showLogisticGraph: async function (iFormula) { + const graphObject = { + type: "graph", + name: testimate.constants.logisticGraphName, + title: `P(${data.xAttData.name} = ${testimate.state.testParams.focusGroupX})`, + dataContext: testimate.state.dataset.name, + xAttributeName: data.yAttData.name, + yAttributeName: testimate.constants.logisticGroupAttributeName, + } + + const theMessage = { + action: "create", + resource: "component", + values: graphObject, + } + + try { + const result = await codapInterface.sendRequest(theMessage); + + } catch (msg) { + alert(`trouble showing the logistics graph ${msg}`); + } + }, + + hideLogisticGraph: function () { + const theMessage = { + action: "delete", + resource: `component[${testimate.constants.logisticGraphName}]` + } + + const result = codapInterface.sendRequest(theMessage); + + }, + + + updateDatasetForLogisticGroups: async function (iValue, iAxis) { + + const theVariable = (iAxis === "X") ? data.xAttData.name : data.yAttData.name; + const theFormula = `if (${theVariable} = "${iValue}", 1, 0)`; + + const newAttributeInfo = { + name: testimate.constants.logisticGroupAttributeName, + title: `${data.xAttData.name} = ${testimate.state.testParams.focusGroup}`, + type: "numeric", + description: `equal to 1 if ${data.xAttData.name} = ${testimate.state.testParams.focusGroup}, zero otherwise`, + editable: false, + formula: theFormula, + hidden: true + } + const getInfoMessage = { + action: "get", + resource: `dataContext[${testimate.state.dataset.name}]` + } + + // figure out which collection the target attribute is in + + let useThisCollection = null; // name of the target collection + + try { + const theInfo = await codapInterface.sendRequest(getInfoMessage); + if (theInfo.success) { + theInfo.values.collections.forEach(coll => { + coll.attrs.forEach(attr => { + if (attr.name === data.xAttData.name) { + useThisCollection = coll.name; + } + }) + }) + } else { + alert(`request for dataset info failed in connect.js`); + } + + } catch (msg) { + alert(`could not get dataset info for [${testimate.state.dataset.name}] in connect.js...${msg}`) + } + + + const newAttMessage = { + action: "create", + resource: `dataContext[${testimate.state.dataset.name}].collection[${useThisCollection}].attribute`, + values: newAttributeInfo + } + + try { + const newAttResult = await codapInterface.sendRequest(newAttMessage); + + } catch (msg) { + alert(`connect.js updateDatasetForLogisticGroups: could not make new 0/1 attribute`); + } + return theFormula; // for diagnostics + }, + + /** + * + * @param iExtras Extra attributes to be emitted, with values. These are typically the + * high-level attribtes when emitting hierarchically. + * @returns {Promise} + */ + emitTestData: async function (iExtras = {}) { + + // make a new output dataset if necessary + // todo: test for dataset existence (user may have deleted it) + if (testimate.state.testID !== testimate.state.mostRecentEmittedTest) { + await this.deleteOutputDataset(); + + const theMessage = { + action: "create", + resource: "dataContext", + values: this.constructEmitDatasetObject(), + } + try { + const result = await codapInterface.sendRequest(theMessage); + if (result.success) { + console.log(`success creating dataset, id=${result.values.id}`); + } else { + console.log(`problem creating dataset`); + } + } catch (msg) { + alert(`problem creating dataset: ${testimate.constants.emittedDatasetName}`); + } + } + + // add any "extra" attributes + + await this.addExtraAttributesToEmittedDataset(iExtras); + + // now emit one item... + + let theItemValues = Object.assign({}, iExtras); + const theTest = testimate.theTest; + const theConfig = theTest.theConfig; + const emittedAttributeNames = theConfig.emitted.split(","); + + console.log(` emitting a case with N = ${theTest.results.N}`); + + // first list the standard attributes (parameters, mostly) + + let theStandardAttributes = {}; + theStandardAttributes[localize.getString("attributeNames.outcome")] = testimate.state.x.name; + theStandardAttributes[localize.getString("attributeNames.predictor")] = + (testimate.predictorExists()) ? testimate.state.y.name : ""; + theStandardAttributes[localize.getString("attributeNames.procedure")] = theConfig.name; + + Object.assign(theItemValues, theStandardAttributes); +/* + Object.assign( + theItemValues, + { + outcome: testimate.state.x.name, + predictor: (testimate.predictorExists()) ? testimate.state.y.name : "", + procedure: theConfig.name, + } + ); +*/ + + // then add "results" values + + emittedAttributeNames.forEach(att => { + const translatedAttributeName = localize.getString(`attributeNames.${att}`); + + if (theTest.results.hasOwnProperty(att)) { + theItemValues[translatedAttributeName] = theTest.results[att] + } else { // not a result? Maybe it's a parameter!! + switch (att) { + case "sign": + theItemValues[translatedAttributeName] = testimate.state.testParams.theSidesOp; + break; + default: + theItemValues[translatedAttributeName] = testimate.state.testParams[att] + break; + } + + } + }); + + + const itemMessage = { + action: 'create', + resource: `dataContext[${testimate.constants.emittedDatasetName}].item`, + values: theItemValues, // sending ONE item + } + const result = await codapInterface.sendRequest(itemMessage); + if (result.success) { + console.log(`success creating item id=${result.itemIDs[0]}`); + } else { + console.log(`problem creating item`); + } + this.makeTableAppear(); + }, + + + constructEmitDatasetObject: function () { + let out = {}; + + if (testimate.state.testID) { + testimate.state.mostRecentEmittedTest = testimate.state.testID; + const theConfig = Test.configs[testimate.state.testID]; + + // first construct the "attrs" array + let theAttrs = []; + theAttrs.push({ + // name: "outcome", + name: localize.getString("attributeNames.outcome"), + title: localize.getString("attributeNames.outcome"), + type: "categorical", + description: localize.getString("attributeDescriptions.outcome") + }); + if (testimate.predictorExists()) { + theAttrs.push({ + // name: "predictor", + name: localize.getString("attributeNames.predictor"), + title: localize.getString("attributeNames.predictor"), + type: "categorical", + description: localize.getString("attributeDescriptions.predictor") + }); + } + theAttrs.push({ + // name: "procedure", + name: localize.getString("attributeNames.procedure"), + title: localize.getString("attributeNames.procedure"), + type: "categorical", + description: localize.getString("attributeDescriptions.procedure") + }); +/* + if (testimate.state.testParams.theSidesOp) { + theAttrs.push({ + name: "sign", + title: localize.getString("attributeNames.sign"), + type: "categorical", + description: localize.getString("attributeDescriptions.sign") + }); + } + theAttrs.push({ + name: "value", + title: localize.getString("attributeNames.value"), + type: "numeric", + precision: 3, + description: localize.getString("attributeDescriptions.value") + }); +*/ + + theConfig.emitted.split(",").forEach(att => { + // const theName = att; + const theName = localize.getString(`attributeNames.${att}`); + const theTitle = localize.getString(`attributeNames.${att}`); + const theTip = localize.getString(`attributeDescriptions.${att}`); + theAttrs.push({ + name: theName, title: theTitle, type: 'numeric', + description: theTip, precision: 4 + }); + }); + + // this will become the "values" item in the call + out = { + name: testimate.constants.emittedDatasetName, + title: localize.getString("datasetName"), // testimate.constants.emittedDatasetName, + collections: [{ + name: localize.getString("datasetName"), // testimate.constants.emittedDatasetName, + title: localize.getString("datasetName"), + attrs: theAttrs + }] + }; + } + return out; + }, + + addExtraAttributesToEmittedDataset : async function(iExtras) { + let theAtts = []; + let attList = []; + + Object.keys(iExtras).forEach( k => { + const thisAtt = { + name : k + } + theAtts.push(thisAtt); + attList.push(k); + }) + + const theMessage = { + action : "create", + resource : `dataContext[${testimate.constants.emittedDatasetName}].collection[${testimate.constants.emittedDatasetName}].attribute`, + values : theAtts + } + + try { + const result = await codapInterface.sendRequest(theMessage); + if (result.success) { + console.log(`added ${attList.join(', ')} to emit dataset`); + } + } catch (msg) { + console.log(`trouble adding extra attributes to emitted dataset: ${msg}`); + } + + }, + + deleteOutputDataset: async function () { + const theMessage = { + action: "delete", + resource: `dataContext[${testimate.constants.emittedDatasetName}]`, + }; + + try { + const result = await codapInterface.sendRequest(theMessage); + } catch (msg) { + alert(`problem deleting dataset: ${testimate.constants.emittedDatasetName} : ${msg}`); + } + }, + + makeTableAppear: function () { + const caseTableObject = { + type: `caseTable`, + dataContext: testimate.constants.emittedDatasetName, + }; + + const message = { + action: 'create', + resource: `component`, + values: caseTableObject, + }; + + codapInterface.sendRequest(message); + }, + + /** + * Kludge to ensure that a dataset is reorg-able. + * + * @returns {Promise} + */ + allowReorg: async function () { + const tMutabilityMessage = { + "action": "update", + "resource": "interactiveFrame", + "values": { + "preventBringToFront": false, + "preventDataContextReorg": false + } + }; + + codapInterface.sendRequest(tMutabilityMessage); + }, + + retrieveTopLevelCases: async function () { + let out = {}; + + const topCollection = data.sourceDatasetInfo.collections[0]; + + const getTopCasesMessage = { + "action": "get", + resource: `dataContext[${data.sourceDatasetInfo.name}].collection[${topCollection.name}].caseFormulaSearch[true]` + } + try { + const result = await codapInterface.sendRequest(getTopCasesMessage); + if (result.success) { + out = result.values; + } else { + console.log(`get top collection cases worked, but failed!`); + } + } catch (msg) { + console.log(`trouble getting top collection cases: ${msg}`); + } + + return out; + }, + + getSourceHierarchyInfo: function () { + return { + nCollections: this.sourceDatasetInfo.collections.length, + topLevelCases: [ + "a", "b", + ] + } + } + + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/data.js b/eepsmedia/plugins/testimate/src/data.js new file mode 100644 index 00000000..39a980e1 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/data.js @@ -0,0 +1,329 @@ +const data = { + + dirtyData: true, + secondaryCleanupNeeded: false, + + allCODAPitems: [], // array retrieved from CODAP + topCases: [], + xAttData: null, + yAttData: null, + + hasRandom: false, + isGrouped: false, + sourceDatasetInfo: {}, + + + + /** + * called from testimate.refreshDataAndTestResults(). + * + * Before we write anything on the screen, we ensure that the data we have is current. + * + * @returns {Promise} + */ + updateData: async function () { + if (testimate.state.dataset) { + + if (this.dirtyData) { + this.sourceDatasetInfo = await connect.getSourceDatasetInfo(testimate.state.dataset.name); + this.hasRandom = this.sourceDSHasRandomness(); + this.isGrouped = this.sourceDSisHierarchical(); + + this.topCases = (this.isGrouped) ? await connect.retrieveTopLevelCases() : []; + + await this.retrieveAllItemsFromCODAP(); + } + } + }, + + /* Coping with getting data from CODAP and responding to changes */ + + /** + * called from this.updateData() + * + * We KNOW the dataset exists and the data are dirty, + * + * @returns {Promise} + */ + retrieveAllItemsFromCODAP: async function () { + if (testimate.state.x) { + this.allCODAPitems = await connect.getAllItems(); // this.dataset is now set as array of objects (result.values) + if (this.allCODAPitems) { + } + } else { + console.log(`no x variable`); + } + }, + + /** + * Construct xAttData and yAttData, the INTERNAL Arrays of the data in each attribute. + * + * Those constructors evaluate the values to tell whether the attributes are numeric or categorical. + * We need this in order to figure out which tests are appropriate, + * and (importantly) to set a test if it has not yet been set. + * + * @param xName + * @param yName + * @param data + * @returns {Promise} + */ + makeXandYArrays : async function(data) { + if (testimate.state.x) { + this.xAttData = new AttData(testimate.state.x.name, data); + if (!testimate.state.focusGroupDictionary[this.xAttData.name]) { + testimate.state.testParams.focusGroupX = testimate.setFocusGroup(this.xAttData, null); + } + } + if (testimate.state.y) { + this.yAttData = new AttData(testimate.state.y.name, data); + testimate.state.testParams.focusGroupY = testimate.setFocusGroup(this.yAttData, null); + } + if (this.xAttData) console.log(` made xAttData (${this.xAttData.theRawArray.length})`); + }, + + removeInappropriateCases: async function () { + + if (!testimate.theTest) return; + + let newXArray = [] + let newYArray = [] + + const paired = Test.configs[testimate.theTest.testID].paired; + + + // make intermediate arrays that have only the right type of values (e.g., numeric) + // same length as original! + + let xIntermediate = []; + if (testimate.state.x) { + const xMustBeNumeric = (testimate.state.dataTypes[testimate.state.x.name] === 'numeric'); + this.xAttData.theRawArray.forEach(xx => { + if (xMustBeNumeric) { + xIntermediate.push(typeof xx === 'number' ? xx : null); + } else { + xIntermediate.push(xx); // strings and nulls + } + }) + } + + let yIntermediate = []; + if (testimate.state.y) { + const yMustBeNumeric = (testimate.state.dataTypes[testimate.state.y.name] === 'numeric'); + this.yAttData.theRawArray.forEach(xx => { + if (yMustBeNumeric) { + yIntermediate.push(typeof xx === 'number' ? xx : null); + } else { + yIntermediate.push(xx); // strings and nulls + } + }) + } + + // now go through the intermediate arrays prudently eliminating null values + + const xLim = xIntermediate.length; + const yLim = yIntermediate.length; + let i = 0; + + while (i < xLim || i < yLim) { + const X = i < xLim ? xIntermediate[i] : null; + const Y = i < yLim ? yIntermediate[i] : null; + + if (paired) { + if (X !== null && Y !== null) { + newXArray.push(X); + newYArray.push(Y); + } + } else { + if (X !== null) newXArray.push(X); + if (Y !== null) newYArray.push(Y); + } + + i++; + } + + this.xAttData.theArray = newXArray; + if (testimate.state.y) this.yAttData.theArray = newYArray; + + console.log(` cleaned xAttData (${this.xAttData.theArray.length})`); + + if (this.xAttData.theArray.length < 20) + console.log(`cleaned x = ${JSON.stringify(this.xAttData.theArray)} \ncleaned y = ${JSON.stringify(this.yAttData.theArray)}`) + }, + + /** + * CODAP has told us that a case has changed. + * We set the dirty data flag and ask to be redrawn. + * This will cause a re-get of all data and a re-analysis. + * + * @param iMessage + * @returns {Promise} + */ + handleCaseChangeNotice: async function (iMessage) { + const theOp = iMessage.values.operation + let tMess = theOp; + // console.log(`start ${tMess}`); + switch (theOp) { + case 'createCases': + case 'updateCases': + case 'deleteCases': + case `dependentCases`: // fires on rerandomize + + tMess += " *"; + data.dirtyData = true; // "this" is the notification, not "data" + if (testimate.OKtoRespondToCaseChanges) await testimate.refreshDataAndTestResults(); + break; + + case `updateAttributes`: + // includes attribute name change! + const theUpdatedAtts = iMessage.values.result.attrs; // array of objects, form {name : newName...} + theUpdatedAtts.forEach(att => { + if (testimate.state.x && att.id === testimate.state.x.id) { // saved id of x-attribute + const oldName = testimate.state.x.name; + console.log(`att X changing from ${oldName} to ${att.title}`); + testimate.state.x.title = att.title; // new name + testimate.state.x.name = att.name; // new name + testimate.state.dataTypes[att.name] = testimate.state.dataTypes[oldName]; + } + if (testimate.state.y && att.id === testimate.state.y.id) { // save id of y-attribute + const oldName = testimate.state.y.name; + console.log(`att Y changing from ${oldName} to ${att.title}`); + testimate.state.y.title = att.title; // new name + testimate.state.y.name = att.name; // new name + testimate.state.dataTypes[att.name] = testimate.state.dataTypes[oldName]; + } + }) + data.dirtyData = true; + if (testimate.OKtoRespondToCaseChanges) await testimate.refreshDataAndTestResults(); + break; + + case `deleteAttributes`: + case `createAttributes`: + data.dirtyData = true; + if (testimate.OKtoRespondToCaseChanges) await testimate.refreshDataAndTestResults(); + break; + + default: + break; + } + // console.log(`end ${tMess}`); + + }, + + handleAttributeChangeNotice: async function (iMessage) { + console.log(`attribute change!`); + }, + + sourceDSHasRandomness: function () { + let out = false; + + if (this.sourceDatasetInfo) { + this.sourceDatasetInfo.collections.forEach(c => { + c.attrs.forEach(a => { + const f = a.formula; + if (f && f.indexOf("random") > -1) { + out = true; + } + }) + }) + } + + return out; + }, + + sourceDSisHierarchical: function () { + if (this.sourceDatasetInfo) { + return (this.sourceDatasetInfo.collections.length > 1); + } + return null; + }, + + filterGroupCases: function(theWholeDataset, theFilterValues) { + out = []; + theWholeDataset.forEach( d => { + const theItem = d.values; + let matches = true; + Object.keys(theFilterValues).forEach(k=>{ + if (theItem[k] !== theFilterValues[k]) { + matches = false; + } + }) + if (matches) { + out.push({values: theItem}); + } + }) + + return out; + }, + + /** + * from https://stackoverflow.com/questions/175739/how-can-i-check-if-a-string-is-a-valid-number + * @param str + * @returns {boolean} + */ + isNumericString: function (str) { + if (typeof str != "string") return false; // we only process strings! + return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... + !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail + }, + +} + +class AttData { + constructor(iAttName, iData) { + this.name = iAttName ? iAttName : null; + this.theRawArray = []; + this.theArray = []; // stays empty in constructor + this.valueSet = new Set(); + this.missingCount = 0; + this.numericCount = 0; + this.nonNumericCount = 0; + this.defaultType = ""; + + iData.forEach(aCase => { // begin with raw CODAP data, look at each case + + const rawDatum = aCase.values[this.name]; + if (rawDatum === null || rawDatum === '' || rawDatum === undefined) { + this.theRawArray.push(null); // substitute null for any missing data + this.missingCount++; + } else if (typeof rawDatum === "number") { // numbers stay type number + this.theRawArray.push(rawDatum); + this.numericCount++; + this.valueSet.add(rawDatum); + } else if (data.isNumericString(rawDatum)) { // strings that can be numbers get stored as numbers + const cooked = parseFloat(rawDatum); + this.theRawArray.push(cooked); + this.numericCount++; + this.valueSet.add(cooked); + } else { // non-numeric // non-numeric strings are strings + this.theRawArray.push(rawDatum); + this.nonNumericCount++; + this.valueSet.add(rawDatum); + } + }); + + // set the type of this attribute (numeric or categorical) + + let defType = null; + if (this.numericCount > this.nonNumericCount) defType = 'numeric'; + else if (this.valueSet.size > 0) defType = 'categorical'; + + this.defaultType = defType; + if (!testimate.state.dataTypes[this.name]) testimate.state.dataTypes[this.name] = this.defaultType; + } + + + isNumeric() { + return testimate.state.dataTypes[this.name] === 'numeric'; + } + + isCategorical() { + return testimate.state.dataTypes[this.name] === 'categorical'; + } + + isBinary() { + return (this.valueSet.size === 2 || this.valueSet.size === 1); + } + + +} + diff --git a/eepsmedia/plugins/testimate/src/dropManager.js b/eepsmedia/plugins/testimate/src/dropManager.js new file mode 100644 index 00000000..b9f63d10 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/dropManager.js @@ -0,0 +1,85 @@ + + +testimate.dropManager = { + + currentlyDraggingCODAPAttribute : false, + currentlyOverDIV : null, + + handleDragDrop : async function(iMessage) { + + switch(iMessage.values.operation) { + case `dragstart`: + this.currentlyDraggingCODAPAttribute = true; + console.log(` drag start`); + break; + case `dragend`: + this.currentlyDraggingCODAPAttribute = false; + testimate.dropManager.highlightNone(); + console.log(` drag end`); + break; + case `drag`: + testimate.dropManager.handleDrag(iMessage.values.position); + break; + case `drop`: + testimate.copeWithAttributeDrop( + iMessage.values.context, + iMessage.values.attribute, + iMessage.values.position + ); + break; + case `dragenter`: + console.log(` drag enter`); + testimate.dropManager.highlightNear(); + break; + case `dragleave`: + testimate.dropManager.highlightNone(); + console.log(` drag leave`); + break; + } + }, + + getAttributeUnderCursor : function(iWhere) { + const theElement = document.elementFromPoint(iWhere.x, iWhere.y); + const theFirstClasslist = theElement.classList; + if (theFirstClasslist.contains("attributeHint") || + theFirstClasslist.contains("attributeName") || + theFirstClasslist.contains("attributeControls")) { + + } + }, + + handleDrag : function(iWhere) { + const currentElement = document.elementFromPoint(iWhere.x, iWhere.y); + // console.log(` drag over element [${currentElement.id}]`); + if (currentElement) { + const theElement = currentElement.closest('#xDIV, #yDIV'); + if (theElement) { + if (theElement === ui.xDIV || theElement === ui.yDIV) { + if (this.currentlyOverDIV && (theElement != this.currentlyOverDIV)) { + this.currentlyOverDIV.classList.replace(`drag-over`, `drag-near`); + console.log(` change drop zone to ${theElement.id}`); + } + this.currentlyOverDIV = theElement; + this.currentlyOverDIV.className = `drag-over`; + + } else { + this.highlightNear(); + } + } + } + }, + + highlightNear : function () { + ui.xDIV.className = `drag-near`; + ui.yDIV.className = `drag-near`; + }, + + highlightNone: function() { + ui.xDIV.className = (testimate.state.x && testimate.state.x.name) ? `drag-none` : `drag-empty`; + ui.yDIV.className = (testimate.state.y && testimate.state.y.name) ? `drag-none` : `drag-empty`; + this.currentlyOverDIV = null; + + }, + + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/handlers.js b/eepsmedia/plugins/testimate/src/handlers.js new file mode 100644 index 00000000..152f1b2b --- /dev/null +++ b/eepsmedia/plugins/testimate/src/handlers.js @@ -0,0 +1,254 @@ +const handlers = { + + getPluginState : function() { + return { + success: true, + values: { + store: testimate.state, + } + }; + }, + + restorePluginFromStore: function(iStorage) { + if (iStorage) { + testimate.state = iStorage.store; + } + }, + + /** + * User has clicked a button that changes whether a test is one- or two-sided + * todo: remove this in favor of changeSides12? + */ + changeTestSides: function () { + const iSign = document.getElementById(`sidesButton`).value; // 1 or 2 + testimate.state.testParams.sides = (iSign === `≠`) ? 1 : 2; + testimate.refreshDataAndTestResults(); + }, + + changeSides12 : function() { + const newSides = testimate.state.testParams.sides === 1 ? 2 : 1; + testimate.state.testParams.sides = newSides; + testimate.refreshDataAndTestResults(); + }, + + changeConf: function () { + const a = document.getElementById(`confBox`); + testimate.state.testParams.conf = a.value; + testimate.state.testParams.alpha = 1 - testimate.state.testParams.conf / 100; + testimate.refreshDataAndTestResults(); + }, + + changeAlpha: function () { + const a = document.getElementById(`alphaBox`); + testimate.state.testParams.alpha = a.value; + testimate.state.testParams.conf = 100 * (1 - testimate.state.testParams.alpha); + testimate.refreshDataAndTestResults(); + }, + + changeValue: function () { + const v = document.getElementById(`valueBox`); + testimate.state.testParams.value = v.value; + testimate.refreshDataAndTestResults(); + }, + + changeIterations: function () { + const v = document.getElementById(`iterBox`); + testimate.state.testParams.iter = v.value; + testimate.refreshDataAndTestResults(); + }, + + changeRate: function () { + const v = document.getElementById(`rateBox`); + testimate.state.testParams.rate = v.value; + testimate.refreshDataAndTestResults(); + }, + + changeEmitMode: function() { + ui.emitMode = document.querySelector("input[name='emitMode']:checked").value; + + // set emitMode to single if you're trying something impossible + if (this.emitMode === 'random' && !data.hasRandom) { + this.emitMode = 'single'; + alert("Can't emit using rerandomizing if there are no random attributes"); // todo: localize + } + if (this.emitMode === 'hierarchical' && !data.isGrouped) { + this.emitMode = 'single'; + alert("Can't emit for each subgroups if there are no subgroups"); // todo: localize + } + + testimate.refreshDataAndTestResults(); + }, + + changeRandomEmitNumber: function () { + const v = document.getElementById(`randomEmitNumberBox`); + testimate.state.randomEmitNumber = v.value; + testimate.refreshDataAndTestResults(); + }, + + changeLogisticRegressionProbe: function () { + const LRP = document.getElementById(`logisticRegressionProbeBox`); + testimate.state.testParams.probe = LRP.value; // need for state and restore + testimate.refreshDataAndTestResults(); + }, + + changeTest: function () { + const T = document.getElementById(`testMenu`); + testimate.makeFreshTest(T.value); // the testID, need for state and restore + testimate.refreshDataAndTestResults(); + }, + + changeFocusGroupX: function () { + const initialGroup = testimate.state.testParams.focusGroupX; + const valueSet = [...data.xAttData.valueSet]; + const nextValue = this.nextValueInList(valueSet, initialGroup); + testimate.state.testParams.focusGroupX = testimate.setFocusGroup(data.xAttData, nextValue); + testimate.refreshDataAndTestResults(); + }, + + changeFocusGroupY: function () { + const initialGroup = testimate.state.testParams.focusGroupY; + const valueSet = [...data.yAttData.valueSet]; + const nextValue = this.nextValueInList(valueSet, initialGroup); + testimate.state.testParams.focusGroupY = testimate.setFocusGroup(data.yAttData, nextValue); + testimate.refreshDataAndTestResults(); + }, + + reverseTestSubtraction : function() { + testimate.state.testParams.reversed = !testimate.state.testParams.reversed; + testimate.refreshDataAndTestResults(); + }, + + /** + * Change the TYPE (categorical or numeric = CN) of the attribute + * @param iXY + */ + changeCN: function (iXY) { + const aName = (iXY === 'x') ? testimate.state.x.name : testimate.state.y.name; + const newType = (testimate.state.dataTypes[aName] === 'numeric' ? 'categorical' : 'numeric'); + testimate.state.dataTypes[aName] = newType; + testimate.refreshDataAndTestResults(); + }, + + + changeGoodnessProp: function(iLast) { + console.log(`changing goodness prop for ${iLast}`); + const theTest = testimate.theTest; + + let propSum = 0; + const lastGroup = theTest.results.groupNames[theTest.results.groupNames.length - 1]; + + theTest.results.groupNames.forEach(g => { + let theBoxValue = 0; + if (g !== lastGroup) { + theBoxValue = Number(document.getElementById(`GProp_${g}`).value); + const oldPropSum = propSum + propSum += theBoxValue; + if (propSum > 1) { + theBoxValue = 1 - oldPropSum; + propSum = 1; + } + } else { // the last one! + theBoxValue = 1 - propSum; + const theLastBox = document.getElementById("lastProp"); + theLastBox.innerHTML = ui.numberToString(theBoxValue); + } + testimate.state.testParams.groupProportions[g] = (theBoxValue); + }) + testimate.refreshDataAndTestResults(); + }, + + /** + * for logistic regression + * @param iHowMany how many more iterations + */ + doMoreIterations : function(iHowMany) { + testimate.theTest.moreIterations = iHowMany; + testimate.theTest.newRegression = false; // we will add on + testimate.refreshDataAndTestResults(); + }, + + showLogisticGraph: function() { + const formulas = testimate.theTest.makeFormulaString(); + connect.showLogisticGraph(formulas.longFormula); + }, + + + getNextGroupValue: function(initialValue) { + const valueSet = [...data.xAttData.valueSet]; + + if (initialValue) { + const nextValue = this.nextValueInList(valueSet, initialValue); + return nextValue ? nextValue : null; + } else { + return valueSet[0]; + } + }, + + nextValueInList: function (iList, iValue) { + const iOrig = iList.indexOf(iValue); + const iNext = (iOrig + 1 >= iList.length) ? 0 : iOrig + 1; + return iList[iNext]; + }, + + + /** + * remove the attribute indicated + * @param iXY + */ + trashAttribute: function (iXY) { + console.log(`removing attribute [${iXY}]`); + testimate.state[iXY] = null; + testimate.theTest = null; + data.xAttData = null; + data.yAttData = null; + data.dirtyData = true; + testimate.refreshDataAndTestResults(); + }, + + /** + * emit test results to CODAP + */ + emitSingle: async function () { + + const theTest = testimate.theTest; + console.log(`N = ${theTest.results.N}, P = ${theTest.results.P}`); + await connect.emitTestData({}); + }, + + /** + * re-randomize and then emit results to CODAP. + */ + emitRandom: async function() { + + for (let i = 0; i < testimate.state.randomEmitNumber; i++) { + await connect.rerandomizeSource(testimate.state.dataset.name); + await this.emitSingle(); + } + + testimate.refreshDataAndTestResults(); + }, + + emitHierarchy: async function() { + + for (let i = 0; i < data.topCases.length; i++ ) { + const tc = data.topCases[i]; + + const theTopValues = tc.values; // must match all of these + console.log(`top case ${i}: match values using ${JSON.stringify(theTopValues)}`); + + const oneGroupDataset = data.filterGroupCases(data.allCODAPitems, theTopValues); + if (oneGroupDataset) { + // console.log(`Filtered: ${JSON.stringify(oneGroupDataset)}`) + await data.makeXandYArrays(oneGroupDataset); + + await data.removeInappropriateCases(); + await testimate.theTest.updateTestResults(); // now we've done the test on this subset + await connect.emitTestData(theTopValues); + console.log(`top case ${i}: done emitting ${JSON.stringify(theTopValues)}`); + } + } + + await testimate.refreshDataAndTestResults(); + }, + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/testimate.js b/eepsmedia/plugins/testimate/src/testimate.js new file mode 100644 index 00000000..02009502 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/testimate.js @@ -0,0 +1,240 @@ +const testimate = { + + state: {}, + restoringFromSave: false, + dirtyData: true, + theTest: null, // the actual test instance, for example, a OneSampleP. + compatibleTestIDs : [], + refreshCount : 0, + OKtoRespondToCaseChanges : true, + + initialize: async function () { + console.log(`initializing...`); + + await connect.initialize( ); + await localize.initialize(localize.figureOutLanguage('en')); + ui.initialize(); + + // this.state = codapInterface.getInteractiveState(); // get stored state if any + this.state = {...this.constants.defaultState, ...this.state}; // have all fields in default! + // codapInterface.updateInteractiveState(this.state); // + + + if (this.state.dataset) { + data.dirtyData = true; + await this.restoreState(); + } + + ui.redraw(); + }, + + /** + * This makes sure data is current + */ + refreshDataAndTestResults: async function () { + + this.refreshCount++; + console.log(`refresh data: ${this.refreshCount}`); + if (this.state.dataset) { + await data.updateData(); + await data.makeXandYArrays(data.allCODAPitems); + this.dirtyData = false; // todo: do we need this any more? + + this.checkTestConfiguration(); // ensure that this.theTest holds a suitable "Test" + + if (this.theTest && this.theTest.testID) { + // remember the test parameters for this type of test + testimate.state.testParamDictionary[testimate.theTest.testID] = testimate.state.testParams; + this.adjustTestSides(); // todo: figure out if this is correct; shouldn't we compute the value before we do this? + + data.removeInappropriateCases(); // depends on the test's parameters being known (paired, numeric, etc) + await this.theTest.updateTestResults(); // with the right data and the test, we can calculate these results. + } else { + console.log(`Unexpected: refreshing data and we don't have a test.`) + } + + } else { + console.log(`trying to refresh data but there is no dataset`) + } + + // codapInterface.updateInteractiveState(this.state); + ui.redraw(); + }, + + /** + * Something wrong here; check ui.sidesBoxHTML to see where this is apparently computed?? // todo: attend to sides, >, <, etc! + */ + adjustTestSides : function() { + this.state.testParams.theSidesOp = "≠"; + if (this.state.testParams.sides === 1) { + this.state.testParams.theSidesOp = (this.theTest.results[this.theTest.theConfig.testing] > testimate.state.testParams.value ? ">" : "<"); + } + }, + + checkTestConfiguration: function () { + this.compatibleTestIDs = Test.findCompatibleTestConfigurations(); + + if (this.theTest) { + if (!this.compatibleTestIDs.includes(this.state.testID)) { + // if the current test is incompatible with the current data, + // pick the first compatible one + this.makeFreshTest(this.compatibleTestIDs[0]) + } + } else if (this.compatibleTestIDs.includes(this.state.testID)) { + // there is no current theTest (e.g., we're restoring from save) + // but there is a suitable testID (from the saved state) + this.makeFreshTest(this.state.testID); + } else if (this.compatibleTestIDs.length) { + // it should ALWAYS be possible to find a possible test. + // set theTest to the first one in the list + this.makeFreshTest(this.compatibleTestIDs[0]) + } else { + alert(`somehow, we see no possible test IDs.`); + } + }, + + restoreState: async function () { + + await connect.registerForCaseChanges(this.state.dataset.name); + if (testimate.state.testID) { + this.restoringFromSave = true; + await this.refreshDataAndTestResults(); + } + }, + + makeFreshTest: function (iID) { + testimate.state.testID = iID; + const theConfigs = Test.configs[iID]; + this.theTest = theConfigs.fresh(iID, data.xAttData, data.yAttData); + this.restoringFromSave = false; + }, + + // todo: move to handlers + copeWithAttributeDrop: async function (iDataset, iAttribute, iWhere) { + // const titleElement = document.getElementById(`titleDIV`); + const initialElement = document.elementFromPoint(iWhere.x, iWhere.y); + const theElement = initialElement.closest('#xDIV, #yDIV'); + + if (!this.state.dataset) { + await this.setDataset(iDataset); + } else if (this.state.dataset.name !== iDataset.name) { + await this.setDataset(iDataset); + this.setX(this.emptyAttribute); + this.setY(this.emptyAttribute); // change of dataset, remove attributes + } + + if (theElement === ui.xDIV) { + await this.setX(iAttribute); + } else if (theElement === ui.yDIV) { + await this.setY(iAttribute); + } else if (theElement && !this.state.x.name) { + await this.setX(iAttribute); // set x anywhere if it doesn't exist + } + + data.dirtyData = true; + + await testimate.refreshDataAndTestResults(); + }, + + setDataset: async function (iDataset) { + this.state.dataset = iDataset; + this.state.testID = null; + this.setX(this.emptyAttribute); + this.setY(this.emptyAttribute); // change of dataset, remove attributes + + await connect.registerForCaseChanges(this.state.dataset.name); + await connect.registerForAttributeEvents(this.state.dataset.name); + // await connect.getDatasetInfo(iName); + console.log(`set dataset to ${iDataset.name}`); + }, + + setX: async function (iAtt) { + data.dirtyData = true; + this.state.x = iAtt; // the whole attribute structure, with .name and .title + console.log(`set X to ${iAtt.name}`); + }, + + setY: async function (iAtt) { + data.dirtyData = true; + if (this.state.x) { + this.state.y = iAtt; + console.log(`set Y to ${iAtt.name}`); + } else { + this.setX(iAtt); // always fill x first. + } + }, + + + /** + * Set the value of the "focusGroup" in the test parameters. + * Also, remember it for later. + * + * @param iAttData the attribute data we're looking at + * @param iValue proposed value + * @returns {Promise} + */ + setFocusGroup: function (iAttData, iValue) { + const theName = iAttData.name; + const theValues = [...iAttData.valueSet]; // possible values for groups + const defaultValue = this.state.focusGroupDictionary[theName] ? + this.state.focusGroupDictionary[theName] : + theValues[0]; + + const theValue = theValues.includes(iValue) ? iValue : defaultValue; + + this.state.focusGroupDictionary[theName] = theValue; + + return theValue; + }, + + setLogisticFocusGroup: async function(iAttData, iValue) { + + const theValue = this.setFocusGroup(iAttData, iValue); + + // if this is logistic regression + const theConfig = Test.configs[testimate.state.testID]; + const theAxis = theConfig.groupAxis; // only exists for logistic regression + if (theAxis) { + const f = await connect.updateDatasetForLogisticGroups(theValue, theAxis); + console.log(`changing logistic grouping: new formula : [${f}]`); + } + // done with special logistic treatment + return theValue; + + }, + + predictorExists: function () { + return (testimate.state.y && testimate.state.y.name); + }, + + emptyAttribute: { + name: "", + title: "", + id: -1, + }, + + constants: { + pluginName: `testimate`, + version: `2024g`, + dimensions: {height: 555, width: 444}, + + emittedDatasetName: `tests and estimates`, // for receiving emitted test and estimate results + logisticGroupAttributeName: `_logisticGroup`, // to add to the original dataset + logisticGraphName: "logistic graph", + + defaultState: { + lang: `en`, + dataset: null, // whole dataset info, includes .name + dataTypes: {}, // {'gender' : 'categorical', 'height' : 'numeric', ...} + x: null, // attribute info, complete + y: null, + randomEmitNumber: 10, // number of times you re-randomize by default + testID: null, + testParams: {}, + mostRecentEmittedTest: null, + focusGroupDictionary : {}, + testParamDictionary : {}, + valueDictionary : {}, // records the number in the "value" box + } + } +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/anova.js b/eepsmedia/plugins/testimate/src/tests/anova.js new file mode 100644 index 00000000..6a0adadd --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/anova.js @@ -0,0 +1,190 @@ +class ANOVA extends Test { + + constructor(iID) { + super(iID); + this.results.expected = {}; + this.results.observed = {}; + this.results.values = []; + } + + updateTestResults() { + + const A = data.xAttData.theArray; + const tempG = data.yAttData.theArray; + const G = tempG.map( n => String(n)); // make string values for group names + + const tempNames = [...data.yAttData.valueSet]; + this.results.groupNames = tempNames.map( n => String(n)); // make string values for group names + + this.results.N = A.length; + + if (this.results.N) { + this.results.sum = 0; + this.results.groupNs = new Array(this.results.groupNames.length).fill(0); + + + // calculate group means + this.results.groupSums = new Array(this.results.groupNames.length).fill(0); + this.results.groupMeans = new Array(this.results.groupNames.length).fill(0); + + for (let ix = 0; ix < A.length; ix++) { + let group = this.results.groupNames.indexOf(G[ix]); + this.results.groupNs[group]++; + this.results.groupSums[group] += A[ix]; + this.results.sum += A[ix]; + } + + this.results.mean = this.results.sum / this.results.N; // grand mean + + // calculate group means (loop over groups...) + for (let ix = 0; ix < this.results.groupNames.length; ix++) { + if (this.results.groupNs[ix]) { + const theGM = this.results.groupSums[ix] / this.results.groupNs[ix]; + this.results.groupMeans[ix] = theGM; + } else { + this.results.groupMeans[ix] = null; // the group mean is null if there are no cases in the group. + } + } + + // calculate within-group errors, add between-group errors + + this.results.SSR = 0; // between-group error (sum of squares of regression) + this.results.SSE = 0; // sum of squares of error (within group) + + for (let ix = 0; ix < A.length; ix++) { + let group = this.results.groupNames.indexOf(G[ix]); + const treat = this.results.groupMeans[group] - this.results.mean; // between + const err = A[ix] - this.results.groupMeans[group]; // within + this.results.SSE += err * err; + this.results.SSR += treat * treat; + } + + this.results.SST = this.results.SSR + this.results.SSE; + const theCIparam = 1 - testimate.state.testParams.alpha / 2; // the large number + + this.results.dfTreatment = this.results.groupNames.length - 1; // "numerator" between groups + this.results.dfError = this.results.N - this.results.groupNames.length; // "denominator" within + this.results.dfTotal = this.results.dfError + this.results.dfTreatment; + + this.results.MSTreatment = this.results.SSR / this.results.dfTreatment; + this.results.MSError = this.results.SSE / this.results.dfError; + + this.results.F = this.results.MSTreatment / this.results.MSError; + + this.results.FCrit = jStat.centralF.inv(theCIparam, this.results.dfTreatment, this.results.dfError); // + this.results.P = 1 - jStat.centralF.cdf(this.results.F, this.results.dfTreatment, this.results.dfError); + } + } + + static toggleDS() { + this.openDS = !this.openDS; + console.log(`descriptive details now ${this.openDS ? 'open' : 'closed'}.`); + } + + makeResultsString() { + + const N = this.results.N; + const F = ui.numberToString(this.results.F); + const FCrit = ui.numberToString(this.results.FCrit); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + + const DSdetails = document.getElementById("DSdetails"); + const DSopen = DSdetails && DSdetails.hasAttribute("open"); + const Fdetails = document.getElementById("Fdetails"); + const Fopen = Fdetails && Fdetails.hasAttribute("open"); + + let out = "
";
+        out += localize.getString("tests.anova.testQuestion",
+            testimate.state.x.name, testimate.state.y.name);
+        out += `
N = ${N}, F = ${F}, ${P}
`; + out += `
`; + out += localize.getString("tests.anova.detailsSummary1"); + out += this.makeDescriptiveTable(); + out += `
`; + out += `
`; + out += localize.getString("tests.anova.detailsSummary2"); + out += this.makeANOVATable(); + out += `
α = ${alpha}, F* = ${FCrit}`; + out += `
`; + out += `
`; + return out; + } + + makeANOVATable() { + const dfT = this.results.dfTreatment; + const dfE = this.results.dfError; + const dfTotal = this.results.dfTotal; + const SSR = ui.numberToString(this.results.SSR, 5); + const SSE = ui.numberToString(this.results.SSE, 5); + const SST = ui.numberToString(this.results.SST, 5); + const MST = ui.numberToString(this.results.MSTreatment, 5); + const MSE = ui.numberToString(this.results.MSError, 5); + const F = ui.numberToString(this.results.F); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + + // const treatmentString = `Treatment
(i.e., ${data.yAttData.name})`; + const treatmentString = `${data.yAttData.name}`; + const errorString = localize.getString("error"); + const totalString = localize.getString("total"); + + let theHTML = ""; + theHTML += ""; + theHTML += `` + theHTML += `` + theHTML += `` + theHTML += `
Source(SS)df(MS)FP
${treatmentString}${SSR}${dfT}${MST}${F}${P}
${errorString}${SSE}${dfE}${MSE}
${totalString}${SST}${dfTotal}
` + + return theHTML; + } + + makeDescriptiveTable() { + const meanOfX = localize.getString("tests.anova.meanOfX",testimate.state.x.name) + + let nameRow = `${data.yAttData.name} →`; + let countRow = `${localize.getString("count")}`; + let meanRow = `${meanOfX}`; + + for (let ix = 0; ix < this.results.groupNames.length; ix++) { + nameRow += `${this.results.groupNames[ix]}`; + countRow += `${this.results.groupNs[ix]}`; + meanRow += `${ui.numberToString(this.results.groupMeans[ix], 3)}`; + } + + nameRow += ``; + countRow += ``; + meanRow += ``; + + return `${nameRow}${meanRow}${countRow}
`; + + } + + makeTestDescription() { + return `ANOVA: ${testimate.state.x.name} by ${testimate.state.y.name}`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.anova.menuString", + testimate.state.x.name, testimate.state.y.name) + // return `ANOVA: ${testimate.state.x.name} by ${testimate.state.y.name}`; + } + + makeConfigureGuts() { + const configStart = localize.getString("tests.anova.configStart", + testimate.state.x.name, testimate.state.y.name) + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${configStart}:
 ${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/beta.js b/eepsmedia/plugins/testimate/src/tests/beta.js new file mode 100644 index 00000000..44fc9d3b --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/beta.js @@ -0,0 +1,43 @@ +// Implementation of the Beta probability density function +// Without the use of factorials to overcome the problem of getting inf values +// No external libraries required +// For naive implementation, see beta_naive.js (removed by terickson) +// Roy Hung 2019 + + +const beta = { + PDF: function (x, a, b) { + // Beta probability density function impementation + // using logarithms, no factorials involved. + // Overcomes the problem with large integers + return Math.exp(this.lnPDF(x, a, b)) + }, + + lnPDF: function (x, a, b) { + // Log of the Beta Probability Density Function + return ((a - 1) * Math.log(x) + (b - 1) * Math.log(1 - x)) - this.lnFunc(a, b); + }, + + lnFunc: function (a, b) { + // Log Beta Function + // ln(Beta(x,y)) + let foo = 0.0; + + for (let i = 0; i < a - 2; i++) { + foo += Math.log(a - 1 - i); + } + for (let i = 0; i < b - 2; i++) { + foo += Math.log(b - 1 - i); + } + for (let i = 0; i < a + b - 2; i++) { + foo -= Math.log(a + b - 1 - i); + } + return foo; + }, + + func: function (x, y) { + // Beta Function + // Beta(x,y) = e^(ln(Beta(x,y)) + return Math.exp(this.lnFunc(x, y)); + } +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/binomial.js b/eepsmedia/plugins/testimate/src/tests/binomial.js new file mode 100644 index 00000000..2b19069d --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/binomial.js @@ -0,0 +1,197 @@ +const binomial = + { + + CIbeta: function (n, k, alpha) { + let upper = 1; + let lower = 0; + let upperP = 1 - alpha/2; + let lowerP = alpha/2; + + // calling order: jStat.beta.inv( p, alpha, beta ); + + if (k === 0) { + upper = jStat.beta.inv(upperP, k + 1, n - k); + } else if (k === n) { + lower = jStat.beta.inv(lowerP, k, n - k + 1); + } else { + upper = jStat.beta.inv(upperP, k + 1, n - k); + lower = jStat.beta.inv(lowerP, k, n - k + 1); + } + return [lower, upper]; + }, + + + findCI: function (n, k, pValue) { + let upper = 1; + let lower = 0; + + if (k === 0) { + upper = this.findInverseCDFValue(n, k, pValue, true); + } else if (k === n) { + lower = this.findInverseCDFValue(n, k, pValue, false); + } else { + upper = this.findInverseCDFValue(n, k, pValue / 2, true); + lower = this.findInverseCDFValue(n, k, pValue / 2, false); + } + + return [lower, upper]; // If no exact match is found within the precision limit + }, + + findInverseCDFValue: function (n, k, target, kOrFewer) { + const epsilon = 0.001; + const maxIter = 20; + let low = 0; + let high = 1.0; + let p; + + let itercount = 0; + console.log(`starting iteration N = ${n}, k = ${k}, target = ${target}, ${kOrFewer ? "k or fewer" : "k or more"}`); + while (itercount < maxIter) { + const mid = (low + high) / 2; + p = mid; + + console.log(`it ${itercount} [${low.toFixed(4)}, ${high.toFixed(4)}]`); + const cdfp = binomial.CDF(n, k, p, kOrFewer); + + if (kOrFewer) { + if (Math.abs(target - cdfp) < epsilon) { + console.log(`iteration ${itercount} converges at CDF(${p}) = ${cdfp}`); + return p; + } else if (cdfp > target) { + low = mid; + } else { + high = mid; + } + } else { // k or more + if (Math.abs(target - cdfp) < epsilon) { + console.log(`iteration ${itercount} converges at CDF(${p}) = ${cdfp}`); + return p; + } else if (cdfp < target) { + low = mid; + } else { + high = mid; + } + + } + itercount++; + } + alert(`fell out of loop itercount = ${itercount}`); + return null; // oops + }, + + /* + inverseCDF: function (p, n, successProbability) { + const epsilon = 1e-10; + let low = 0; + let high = n; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const cdfMid = binomial.CDF(mid, n, successProbability); + + if (Math.abs(cdfMid - p) < epsilon) { + return mid; + } else if (cdfMid < p) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return null; // If no exact match is found within the precision limit + }, + */ + + /** + * What is the probability that you have k or fewer (or more) successes in + * n Bernoulli trials + * with probability P + * + * @param n + * @param k + * @param trueP + * @param kOrFewer + * * @returns {number} + * @constructor + */ + CDF: function (n, k, trueP, kOrFewer) { + // Function to calculate the cumulative binomial distribution + /* + const binomialCoefficient = binomial.choose(n, k); + const probability = binomialCoefficient * Math.pow(trueP, k) * Math.pow(1 - trueP, n - k); + */ + + let cumulativeProbability = 0; + + if (kOrFewer) { + for (let i = 0; i <= k; i++) { + cumulativeProbability += binomial.choose(n, i) * Math.pow(trueP, i) * Math.pow(1 - trueP, n - i); + } + } else { // we're calculating the probability for k or MORE + for (let i = n; i >= k; i--) { + cumulativeProbability += binomial.choose(n, i) * Math.pow(trueP, i) * Math.pow(1 - trueP, n - i); + } + } + + return cumulativeProbability; + }, + + choose: function (n, k) { + // Function to calculate binomial coefficient (n choose k) + if (k === 0 || k === n) { + return 1; + } else { + return binomial.choose(n - 1, k - 1) + binomial.choose(n - 1, k); + } + }, + + //` testing + + calculateCDF: function () { + const P = Number(document.getElementById("trueProb").value); + const k = Number(document.getElementById("successes").value); + const N = Number(document.getElementById("samples").value); + + const NKResult = this.choose(N, k); + const CDFResultFewer = this.CDF(N, k, P, true); + + const NPtext = `(${N} ${k}) = ${NKResult}`; + const CDFtext = `result: CDF(${N}, ${k}, at P = ${P}) = ${CDFResultFewer}`; + + document.getElementById("result").innerHTML = `${NPtext}
${CDFtext}`; + + let data = "p,cdf-,cdf+\n"; + for (let p = 0.05; p < 1.0; p += 0.05) { + const cdfFewer = this.CDF(N, k, p, true); + const cdfMore = this.CDF(N, k, p, false); + data += `${p},${cdfFewer},${cdfMore} \n`; + } + + console.log(data); + }, + + calculateCI: function () { + const k = Number(document.getElementById("successes").value); + const N = Number(document.getElementById("samples").value); + const pValue = Number(document.getElementById("pValue").value); + + // const result = binomial.findCI(N, k, pValue); + const resultBeta = binomial.CIbeta(N, k, pValue); + let CItext = `result: CI(${N}, ${k}, p-hat = (${k / N}) for P-value = ${pValue})`; + // CItext += `
 = [${result[0].toFixed(4)}, ${result[1].toFixed(4)}] (brute force)`; + CItext += `
 = [${resultBeta[0].toFixed(4)}, ${resultBeta[1].toFixed(4)}] (beta)`; + + document.getElementById("result").innerHTML = `${CItext}`; + + } + } + + +/* +// Example usage: +const pValue = 0.8; // Probability +const nValue = 10; // Number of trials +const probSuccess = 0.5; // Probability of success in each trial + +const inverseCDFResult = binomialInverseCDF(pValue, nValue, probSuccess); +console.log("Inverse of Cumulative Binomial Distribution:", inverseCDFResult);*/ diff --git a/eepsmedia/plugins/testimate/src/tests/correlation.js b/eepsmedia/plugins/testimate/src/tests/correlation.js new file mode 100644 index 00000000..6de79d14 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/correlation.js @@ -0,0 +1,133 @@ +class Correlation extends Test { + + constructor(iID, iGrouping) { + super(iID); + } + + updateTestResults() { + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + let sumXY = 0; + let sumX = 0; + let sumXX = 0; + let sumYY = 0; + let sumY = 0; + let N = data.xAttData.theArray.length; + const df = N - 2; + + if (N > 2) { + for (let i = 0; i < N; i++) { + // Note how these definitions are REVERSED. + // we want to look at the var in the first position (xAttData) as the dependent variable (Y) + const X = data.yAttData.theArray[i]; + const Y = data.xAttData.theArray[i]; + sumX += X; + sumY += Y; + sumXY += X * Y; + sumXX += X * X; + sumYY += Y * Y; + } + + const slope = (N * sumXY - sumX * sumY) / (N * sumXX - sumX ** 2); + const intercept = (sumY - slope * sumX) / N; + const SDsqError = 1 / (N * (N - 2)) * (N * sumYY - sumY ** 2 - slope ** 2 * (N * sumXX - sumX ** 2)); + const SDsqSlope = N * SDsqError / (N * sumXX - sumX ** 2); + const SDsqIntercept = SDsqSlope / N * sumXX; + const rho = (N * sumXY - sumX * sumY) / + Math.sqrt((N * sumXX - sumX ** 2) * (N * sumYY - sumY ** 2)); + const rsq = rho * rho; + + // test for rho ≠ 0 from https://online.stat.psu.edu/stat501/lesson/1/1.9 + + this.results.N = N; + this.results.df = df; + this.results.tCrit = jStat.studentt.inv(theCIparam, df); // 1.96-ish for 0.95 + this.results.rho = rho; + this.results.rsq = rsq; + + // test correlation against ZERO + this.results.t = rho * Math.sqrt(df/(1 - rsq)); + + // CI calculations, see https://www.statology.org/confidence-interval-correlation-coefficient/ + const zr = Math.log((1 + rho)/(1 - rho)) / 2.0 ; + const halfWidth = this.results.tCrit / Math.sqrt(N - 3); + const L = zr - halfWidth; + const U = zr + halfWidth; + + this.results.CImin = (Math.exp(2 * L) - 1) / (Math.exp(2 * L) + 1); // numeric value + this.results.CImax = (Math.exp(2 * U) - 1) / (Math.exp(2 * U) + 1); // numeric value + + const tAbs = Math.abs(this.results.t); + this.results.P = jStat.studentt.cdf(-tAbs, this.results.df); + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + } + + } + + makeResultsString() { + // const testDesc = `mean of ${testimate.state.x.name}`; + const N = this.results.N; + + const rho = ui.numberToString(this.results.rho); // correlation + const rsq = ui.numberToString(this.results.rsq); // r^2, coeff of deter + + const CImin = ui.numberToString(this.results.CImin); // CI of correlation + const CImax = ui.numberToString(this.results.CImax); + + const df = ui.numberToString(this.results.df); + + const t = ui.numberToString(this.results.t, 3); + const tCrit = ui.numberToString(this.results.tCrit, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + + const theSign = rho >= 0 ? "+" : '-'; + + const X = testimate.state.x.name; + const Y = testimate.state.y.name; + + const DSdetails = document.getElementById("DSdetails"); + const DSopen = DSdetails && DSdetails.hasAttribute("open"); + + const testingSlopePhrase = localize.getString("tests.regression.testingSlope"); + const slopeWord = localize.getString("slope"); + const interceptWord = localize.getString("intercept"); + + let out = "
";
+
+        //  out += `How does (${X}) depend on (${Y})?`
+        out += localize.getString("tests.correlation.testQuestion",
+            X, Y, testimate.state.testParams.theSidesOp, testimate.state.testParams.value.toString());
+        out += `
ρ = ${rho}, r2 = ${rsq}, N = ${N}`; // note reversal! + out += `
t = ${t}, ${P}`; + out += `
${localize.getString("CI")} = [${CImin}, ${CImax}]`; + out += `
df = ${df}, α = ${alpha}, t* = ${tCrit}, ` + out += `
`; + + return out; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.correlation.menuString",testimate.state.x.name, testimate.state.y.name); + } + + makeConfigureGuts() { + const testingCorrelationPhrase = localize.getString("tests.correlation.testingCorrelation"); + + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = "0"; // ui.valueBoxHTML(testimate.state.testParams.value); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${testingCorrelationPhrase} ${sides} ${value}, ${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/goodness.js b/eepsmedia/plugins/testimate/src/tests/goodness.js new file mode 100644 index 00000000..d6d2c024 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/goodness.js @@ -0,0 +1,220 @@ +class Goodness extends Test { + + constructor(iID) { + super(iID); + this.results.expected = {}; + this.results.observed = {}; + this.results.groupNames = []; + if (!testimate.restoringFromSave) { + testimate.state.testParams.groupProportions = {}; + } + + // testimate.state.testParams.sides = 1; + } + + updateTestResults() { + + const A = data.xAttData.theArray; + this.results.N = A.length; + const tempNames = [...data.xAttData.valueSet]; + this.results.groupNames = tempNames.map( n => String(n)); + + testimate.state.testParams.groupProportions = this.getExpectations(); + + this.results.groupNames.forEach( v => { + this.results.observed[v] = 0; + this.results.expected[v] = this.results.N * testimate.state.testParams.groupProportions[v]; + }) + + //`count the observed values in each category + A.forEach( a => { + this.results.observed[a]++; + }) + + // counts array now has all counts. + + this.results.chisq = 0; + + this.results.groupNames.forEach( v => { + const cellValue = (this.results.observed[v] - this.results.expected[v])**2 + / this.results.expected[v]; + this.results.chisq += cellValue; + }) + + const theCIparam = 1 - testimate.state.testParams.alpha / testimate.state.testParams.sides; // the large number + this.results.df = this.results.groupNames.length - 1; + this.results.chisqCrit = jStat.chisquare.inv(theCIparam, this.results.df); // + this.results.P = 1 - jStat.chisquare.cdf(this.results.chisq, this.results.df); + } + + makeResultsString() { + + const N = this.results.N; + const chisq = ui.numberToString(this.results.chisq); + const chisqCrit = ui.numberToString(this.results.chisqCrit); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const df = ui.numberToString(this.results.df, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + + const GFdetails = document.getElementById("GFdetails"); + const GFopen = GFdetails && GFdetails.hasAttribute("open"); + + let out = "
";
+        out += localize.getString("tests.goodness.testQuestion", data.xAttData.name);
+        //  out += `Are the proportions of ${data.xAttData.name} as hypothesized?`;
+        out += `
N = ${N}, ${this.results.groupNames.length} ${localize.getString("groups")}, χ2 = ${chisq}, ${P}`; + out += `
`; + out += localize.getString("tests.goodness.detailsSummary1", testimate.state.testParams.sides); + out += this.makeGoodnessTable(); + out += ` df = ${df}, α = ${alpha}, χ2* = ${chisqCrit}
`; + out += `
`; + + out += `
`; + return out; + } + + makeGoodnessTable() { + + let nameRow = `${data.xAttData.name} =`; + let observedRow = `${localize.getString("observed")}`; + let expectedRow = `${localize.getString("expected")}`; + + this.results.groupNames.forEach( v => { + nameRow += `${v}`; + observedRow += `${this.results.observed[v]}`; + expectedRow += `${ui.numberToString(this.results.expected[v], 3)}`; + }) + + nameRow += ``; + observedRow += ``; + expectedRow += ``; + + return `${nameRow}${observedRow}${expectedRow}
`; + } + + getExpectations() { + let out = {}; + + let needFresh = false; + + const oldGroups = Object.keys(testimate.state.testParams.groupProportions); + // problem here: oldGroups is now an array of STRINGS, even if the keys were numbers. + // (Titanic "Class", {1,2,3} rendered as categorical, now we're doing goodness of fit.) + + const newGroups = this.results.groupNames; + + let sum = 0; + + /* + for each old group, if it's also a new group, + give it that old proportion as a first guess + */ + oldGroups.forEach( old => { + if (newGroups.includes(old)) { // there is a match! + let newVal = testimate.state.testParams.groupProportions[old]; + if (sum + newVal > 1) { + newVal = 1 - sum; + } + out[old] = newVal; + sum += newVal; + } + }) + + // how many do we still have to find? + const leftOut = newGroups.length - Object.keys(out).length; + + /* + for each new group, is it left out? + if so, give it that fraction of what's left to be allocated. + */ + newGroups.forEach(n => { + if (!out.hasOwnProperty(n)) { // haven't done it yet! + out[n] = (1 - sum)/leftOut; + } + }) + + return out; + } + + makeTestDescription( ) { + return `goodness of fit: ${testimate.state.x.name}`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.goodness.menuString",testimate.state.x.name); + // return `goodness of fit for ${testimate.state.x.name}`; + } + + makeConfigureGuts() { + const sides12Button = ui.sides12ButtonHTML(testimate.state.testParams.sides); + const alpha = ui.alphaBoxHTML(testimate.state.testParams.alpha); + + let theHTML = `${localize.getString("tests.goodness.configurationStart")}`; + theHTML += `
 ${alpha} ${sides12Button}`; + + + let nameRow = `${testimate.state.x.name} → `; + let valueRow = `${this.equalExpectationsButton()}`; + + // is the goodness-of-fit configuration details element [extant and] open? + + const GFConfigDetails = document.getElementById("GFConfigDetails"); + const GFConfigOpen = GFConfigDetails && GFConfigDetails.hasAttribute("open"); + + // start the GF details element + + theHTML += `
`; + theHTML += localize.getString("tests.goodness.detailsSummary2"); + + // start the table of values. These are not results per se, but we class the table that way. + + theHTML += `` + + // the last group name will absorb any leftover proportion + const lastGroupName = this.results.groupNames[this.results.groupNames.length - 1]; + this.results.groupNames.forEach( g => { + const theProp = ui.numberToString(testimate.state.testParams.groupProportions[g],3) + nameRow += ``; + valueRow += (g === lastGroupName) ? // (the last one) + `` : + ``; + }) + + theHTML += `${nameRow}${valueRow}
${g}${theProp}
`; + theHTML += `
`; + + return theHTML; + } + + equalExpectationsButton( ) { + const theTip = localize.getString("tips.equalize"); + const theLabel = localize.getString("equalize") + " →"; + return `` + } + + static equalizeExpectations() { + const theProportions = testimate.state.testParams.groupProportions; + const theShares = Object.keys(theProportions).length; + const theEqualShare = 1.0 / theShares; + for (let group in theProportions) { + if (theProportions.hasOwnProperty(group)) { + theProportions[group] = theEqualShare; + } + } + testimate.refreshDataAndTestResults(); + } + + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/independence.js b/eepsmedia/plugins/testimate/src/tests/independence.js new file mode 100644 index 00000000..a807ed1e --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/independence.js @@ -0,0 +1,157 @@ +class Independence extends Test { + + constructor(iID) { + super(iID); + this.results.rowLabels = []; + this.results.columnLabels = []; + this.results.observed = null; + this.results.expected = null; + + // testimate.state.testParams.sides = 1; + } + + updateTestResults() { + + const X = data.xAttData.theArray; // row-attribute data + const Y = data.yAttData.theArray; // column-attribute data + this.results.N = X.length; + + this.results.rowLabels = [...data.xAttData.valueSet]; // x is vertical, row labels + this.results.columnLabels = [...data.yAttData.valueSet]; + + this.results.observed = this.makeZeroMatrix(this.results.columnLabels.length, this.results.rowLabels.length); + this.results.expected = this.makeZeroMatrix(this.results.columnLabels.length, this.results.rowLabels.length); + + this.results.rowTotals = new Array(this.results.rowLabels.length).fill(0); + this.results.columnTotals = new Array(this.results.columnLabels.length).fill(0); + + for (let r = 0; r < this.results.rowLabels.length; r++) { + for (let c = 0; c < this.results.columnLabels.length; c++) { + this.results.observed[c][r] = 0; + } + } + + // loop over all data + // count the observed values in each cell, update row and column totals + + for (let ix = 0; ix < X.length; ix++) { + const row = this.results.rowLabels.indexOf(X[ix]); + const column = this.results.columnLabels.indexOf(Y[ix]); + this.results.observed[column][row]++; + this.results.rowTotals[row]++ + this.results.columnTotals[column]++; + } + + // calculate expected values and chisquare contributions + this.results.chisq = 0; + + for (let r = 0; r < this.results.rowLabels.length; r++) { + for (let c = 0; c < this.results.columnLabels.length; c++) { + this.results.expected[c][r] = this.results.columnTotals[c] * this.results.rowTotals[r] / this.results.N; + const contrib = (this.results.observed[c][r] - this.results.expected[c][r]) ** 2 + / this.results.expected[c][r]; + this.results.chisq += contrib + } + } + + + const theCIparam = 1 - testimate.state.testParams.alpha / testimate.state.testParams.sides; // 2; // the large number + this.results.df = (this.results.rowLabels.length - 1) * (this.results.columnLabels.length - 1); + this.results.chisqCrit = jStat.chisquare.inv(theCIparam, this.results.df); // + this.results.P = 1 - jStat.chisquare.cdf(this.results.chisq, this.results.df); + } + + makeResultsString() { + const N = this.results.N; + const chisq = ui.numberToString(this.results.chisq); + const chisqCrit = ui.numberToString(this.results.chisqCrit); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const df = ui.numberToString(this.results.df, 3); + // const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + + const TIdetails = document.getElementById("TIdetails"); + const TIopen = TIdetails && TIdetails.hasAttribute("open"); + + let out = "
";
+        out += localize.getString("tests.independence.testQuestion",
+            testimate.state.y.name, testimate.state.x.name);
+        out += `
N = ${N}, ${this.results.columnLabels.length} columns by ${this.results.rowLabels.length} rows, ` + out += `χ2 = ${chisq}, ${P}`; + out += `
`; + out += localize.getString("tests.independence.detailsSummary", testimate.state.testParams.sides); + out += this.makeIndependenceTable(); + out += `
df = ${df}, α = ${alpha}, χ2* = ${chisqCrit}
`; + out += `
`; + + out += `
`; + return out; + } + + makeIndependenceTable() { + + let headerRow = `${localize.getString("observed")}
${localize.getString("expected")}${data.yAttData.name} = `; + let tableRows = ""; +/* + let observedRow = `${localize.getString("observed")}`; + let expectedRow = `${localize.getString("expected")}`; +*/ + + // construct a header + + for (let c = 0; c < this.results.columnLabels.length; c++) { + const col = this.results.columnLabels[c]; // the string label + headerRow += `${col}`; // column value in the header + } + headerRow += ``; + + // now loop over rows, making a column inside each... + + for (let r = 0; r < this.results.rowLabels.length; r++) { + const row = this.results.rowLabels[r]; // the string row label + const attLabel = (r === 0) ? `${data.xAttData.name} = ` : ``; + let thisRow = `${attLabel}${row}`; + for (let c = 0; c < this.results.columnLabels.length; c++) { + const exp = ui.numberToString(this.results.expected[c][r], 4); + const col = this.results.columnLabels[c]; // the string label + thisRow += `${this.results.observed[c][r]}
${exp}`; // observed value in the cell + } + thisRow += ``; + tableRows += thisRow; + } + + return `${headerRow}${tableRows}
`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.independence.menuString", + testimate.state.y.name,testimate.state.x.name); + } + + makeConfigureGuts() { + const sides12Button = ui.sides12ButtonHTML(testimate.state.testParams.sides); + + const start = localize.getString("tests.independence.configurationStart", + testimate.state.y.name, testimate.state.x.name); + // const conf = ui.confBoxHTML(testimate.state.testParams.conf); + const alpha = ui.alphaBoxHTML(testimate.state.testParams.alpha); + let theHTML = `${start}:
 ${alpha} ${sides12Button}`; + + return theHTML; + } + + makeZeroMatrix(cols, rows) { + let A = new Array(cols); + for (let c = 0; c < cols; c++) { + A[c] = new Array(rows).fill(0); + } + return A; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/logistic.js b/eepsmedia/plugins/testimate/src/tests/logistic.js new file mode 100644 index 00000000..79b7865f --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/logistic.js @@ -0,0 +1,370 @@ +/** + * Methods for logistic regression. + * + * Math notes! We will be using the function `logisticregression()` below to iterate on this function: + * + * f(x) = 1/(1 + exp(-(b + wx)) + * + * finding values for b and w, which are kinda-sorta slope and intercept, that is, + * a large value for w means that the logistic curve is steeper, + * and a large b means that the place where the curve passes 1/2 and inflects is farther from 0. + * + * For thinking purposes, we can transform that function, using + * + * p = -(b/w) and m = (w/4). (so b = -4mp and w = 4m) + * + * This gives + * + * f(x) = 1/(1 + exp(-4m(x-p)) + * + * which has the happy result that p is the (x) position of that inflection point + * and m is the slope of the curve at that point. + * + * p becomes this.results.pos + * + */ +class Logistic extends Test { + + constructor(iID) { + super(iID); + + //if (!testimate.restoringFromSave) { + testimate.state.testParams.rate = 0.1; + testimate.state.testParams.iter = 100; + testimate.state.testParams.probe = null; // what value of the predictor do we want to find a probability for? + testimate.state.testParams.focusGroupX = null; // what value gets cast as "1"? The rest are "0" + + //} + + this.graphShowing = false; + this.newRegression = true; // would be false if we were addiing on additional iterations + this.moreIterations = 0; // and that's how many! + + if (!testimate.restoringFromSave || !testimate.state.testParams.focusGroupX) { + testimate.state.testParams.focusGroupX = testimate.state.focusGroupDictionary[data.xAttData.name]; + } + + + } + + async updateTestResults() { + testimate.OKtoRespondToCaseChanges = false; + + const X0 = data.xAttData.theArray; + const Y = data.yAttData.theArray; + const N = X0.length; + this.results.N = N; + + if (N !== Y.length) { + alert(`Paired arrays are not the same length! Bogus results ahead!`); + } + + // this will also make the extra column of coded data values if it did not exist before + await testimate.setLogisticFocusGroup(data.xAttData, testimate.state.testParams.focusGroupX); // the first, by default + + const X = X0.map(x => { + return (x === testimate.state.testParams.focusGroupX) ? 1 : 0; + }) + + let iterations = testimate.state.testParams.iter; + + if (this.newRegression) { + // compute mean of Y to give initial value for pos + let theMax = -Infinity; + let theMin = Infinity; + let pos0 = 0; + Y.forEach(y => { + pos0 += y; + if (y > theMax) theMax = y; + if (y < theMin) theMin = y; + }) // add up all the pos + pos0 /= N; // to get the mean position + + console.log(` logistic regression: initial critical position: ${pos0}`); + if (!testimate.state.testParams.probe) testimate.state.testParams.probe = pos0; + this.results.pos = pos0; + this.results.LSlope = 0; + this.results.iterations = 0; + this.results.rangeX = theMax - theMin; + + // note: results.iterations is the total number of iterations (and its get emitted); + // testParams.iter is the number we're running right now + } else { + iterations = this.moreIterations; + this.newRegression = true; // reset! + } + + const theResult + = await this.logisticRegressionUsingCurvature( + X, Y, + testimate.state.testParams.rate, + iterations, // how many we're running now + this.results.LSlope, this.results.pos + ); + + if (this.graphShowing) { + content.showLogisticGraph(this.makeFormulaString().longFormula); + } + + this.results.iterations += Number(iterations); + this.results.LSlope = theResult.currentSlope; + this.results.pos = theResult.currentPos; + this.results.cost = theResult.currentCost; + + testimate.OKtoRespondToCaseChanges = true; + } + + makeFormulaString() { + const longSlope = this.results.LSlope; + const shortSlope = ui.numberToString(this.results.LSlope, 4); + const shortPos = ui.numberToString(this.results.pos, 4); + const longPos = this.results.pos; + + // shortFormula is for screen display, so has the attribute name + // longFormula is for actual use, and uses "x". Avoids trying to insert backtick... + const shortFormula = `1/(1 + e^(-4 * ${shortSlope} * (${data.yAttData.name} - ${shortPos})))`; + const longFormula = `1/(1 + e^(-4 * ${longSlope} * (x - ${longPos})))`; + + return {shortFormula, longFormula}; + } + + makeResultsString() { + const N = this.results.N; + const cost = ui.numberToString(this.results.cost, 4); + const LSlope = ui.numberToString(this.results.LSlope, 4); + const pos = ui.numberToString(this.results.pos, 4); + const LRPbox = ui.logisticRegressionProbeBoxHTML(testimate.state.testParams.probe); + const graphButton = ui.makeLogisticGraphButtonHTML(); + const theFormulas = this.makeFormulaString(); + const theShortFormula = theFormulas.shortFormula; + const theLongFormula = theFormulas.longFormula; + + console.log(theLongFormula); + + const more10button = ` N = ${N}, ${this.results.iterations} ${localize.getString("iterations")}, ${localize.getString("cost")} = ${cost} ${more10button}

`; + + // model + out += `
${localize.getString("tests.logistic.model1", testimate.state.y.name, pos)}.` + out += `
${localize.getString("tests.logistic.model2", LSlope)}`; + out += `
${localize.getString("tests.logistic.probFunctionHead")}` + out += `
prob(${data.xAttData.name} = ${testimate.state.testParams.focusGroupX}) = ${theShortFormula}`; + + out += `

${graphButton} `; + out += ``; + + out += `

`; + out += localize.getString("tests.logistic.probQuery1", testimate.state.x.name, testimate.state.testParams.focusGroupX); + out += `
${localize.getString("tests.logistic.probQuery2", testimate.state.y.name)} = ${LRPbox}`; + + if (testimate.state.testParams.probe) { + const z = 4 * LSlope * (testimate.state.testParams.probe - pos); + const probNumber = this.sigmoid(z); + let probString = "0.000" + if (probNumber > 0.0000001) { + probString = ui.numberToString(probNumber, 3); + } + + out += ` P(${testimate.state.testParams.focusGroupX}) = ${probString}`; + } + out += ``; + return out; + } + + makeTestDescription() { + return `logistic regression: ${data.xAttData.name} as a function of ${data.yAttData.name}`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.logistic.menuString", + testimate.state.x.name, testimate.state.y.name); + // return `logistic regression: ${data.xAttData.name} as a function of ${data.yAttData.name}`; + } + + makeConfigureGuts() { + const rate = ui.rateBoxHTML(testimate.state.testParams.rate, 1.0, 0.01); + const iter = ui.iterBoxHTML(testimate.state.testParams.iter); + const group = ui.focusGroupButtonXHTML(testimate.state.testParams.focusGroupX); + const showGraph = ui.makeLogisticGraphButtonHTML(); + + const rateWord = localize.getString("rate"); + const iterationsWord = localize.getString("iterations"); + + let theHTML = localize.getString("tests.logistic.configStart", + testimate.state.x.name, group, testimate.state.y.name); + + theHTML += `
 ${rateWord} = ${rate} ${iterationsWord} = ${iter}`; + return theHTML; + } + + sigmoid(z) { + return 1 / (1 + Math.exp(-z)); + } + + + async logisticRegressionUsingCurvature(outcome, predictor, alpha, iterations, slope0 = 0, pos0 = 0) { + + function sigmoid(z) { + return 1 / (1 + Math.exp(-z)); + } + + function oneCost(xx, yy, slope, pos) { + const z = 4 * slope * (xx - pos); + const prediction = sigmoid(z); + let dCost = 0 + if (prediction !== 0 && prediction !== 1) { + dCost = yy * Math.log(prediction) + (1 - yy) * Math.log(1 - prediction); + } + return dCost; + } + + function cost(slope, pos) { + let cost = 0; + + for (let i = 0; i < outcome.length; i++) { + cost -= oneCost(predictor[i], outcome[i], slope, pos); + } + return cost; + } + + function getCost(slope, pos) { + const theCost = cost(slope, pos); + return theCost; + } + + function gradientPartials(slope, pos, hs, hp) { + + const theCost = getCost(slope, pos), + costPlusSlope = getCost(slope + hs, pos), + costMinusSlope = getCost(slope - hs, pos), + costPlusPos = getCost(slope, pos + hp), + costMinusPos = getCost(slope, pos - hp); + + const dCostdSlope = (costPlusSlope - costMinusSlope) / (2 * hs), + dCostdSlopePlus = (costPlusSlope - theCost) / hs, + dCostdSlopeMinus = (theCost - costMinusSlope) / hs; + const dCostdPos = (costPlusPos - costMinusPos) / (2 * hp), + dCostdPosPlus = (costPlusPos - theCost) / hp, + dCostdPosMinus = (theCost - costMinusPos) / hp; + + + const d2CostdSlope2 = (dCostdSlopePlus - dCostdSlopeMinus) / hs; + const d2CostdPos2 = (dCostdPosPlus - dCostdPosMinus) / hp; + + return {theCost, dCostdSlope, d2CostdSlope2, dCostdPos, d2CostdPos2}; + } + + function descendPartialOneIteration(slope, pos, alpha) { + + const theResults = testimate.theTest.results; + + const hs = 1 / theResults.rangeX / 1.0e4; // h for slope calculations + const hp = theResults.rangeX / 1.0e4; // h for p (pos) calculations + + const gradientStuff = gradientPartials(slope, pos, hs, hp); + const projectedDSlope = (gradientStuff.d2CostdSlope2 !== 0) ? -gradientStuff.dCostdSlope / gradientStuff.d2CostdSlope2 : 0, + projectedDPos = (gradientStuff.d2CostdPos2 !== 0) ? -gradientStuff.dCostdPos / gradientStuff.d2CostdPos2 : 0; + + const + newSlope = slope + projectedDSlope * alpha, + newPos = pos + projectedDPos * alpha, + theCost = gradientStuff.theCost; + + return {newSlope, newPos, theCost, hs, hp}; + } + + // Done with defining functions. Actual method starts here! + + let record = "iter, m, p, cost, hm, hp"; + let currentSlope = slope0; + let currentPos = pos0; + let currentCost = 0; + + for (let iter = 1; iter <= iterations; iter++) { + const newVals = descendPartialOneIteration(currentSlope, currentPos, alpha); + + currentSlope = newVals.newSlope; + currentPos = newVals.newPos; + currentCost = newVals.theCost; + + if (iter % 17 === 0 || iter < 6) { + record += `\n${iter}, ${currentSlope}, ${currentPos}, ${currentCost}, ${newVals.hs}, ${newVals.hp}`; + } + } + + console.log('\n' + record); + return {currentSlope, currentPos, currentCost}; + } + + /* + GPT_LogisticRegression(x, y, alpha, iterations) { + // Initialize weights and bias + let w = 0; + let b = 10; + let slope = w / 4; + let pos = -b / w; + let record = ""; + + // Number of samples + const N = x.length; + + record += "iter, m, p, costper, hs, hp"; + + for (let iter = 1; iter < iterations; iter++) { + let cost = 0; + let dw = 0; + let db = 0; + + for (let i = 0; i < N; i++) { + const xi = x[i]; + const yi = y[i]; + + // Compute prediction using the sigmoid function + const z = w * xi + b; + const prediction = this.sigmoid(z); + + // Compute cost. It's the log of the absolute distance of the point from the model + // note that yi is either zero or one, so only one term survives. + // + cost -= yi * Math.log(prediction) + (1 - yi) * Math.log(1 - prediction); + + // Compute gradients + const gradient = prediction - yi; + dw += xi * gradient; + db += gradient; + } + + // Update weights and bias + slope = w / 4; + pos = -b / w; + + + if (iter % 100 === 0) { + record += `\n${iter},${slope},${pos},${cost / N}`; + } + // Print the cost for every 1000 iterations + /!* + if (iter % 1000 === 0) { + console.log(`Iteration ${iter}: Cost = ${cost / N}`); + } + *!/ + } + + console.log('\n' + record); + + return {w, b}; + } + */ + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/one-sample-p.js b/eepsmedia/plugins/testimate/src/tests/one-sample-p.js new file mode 100644 index 00000000..7f49b9e3 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/one-sample-p.js @@ -0,0 +1,159 @@ + +class OneSampleP extends Test { + + usingBinomial = false; + + constructor(iID) { + super(iID); + + // get a default "group" -- the value we count as "success" for proportions + if (!testimate.restoringFromSave || !testimate.state.testParams.focusGroupX) { + testimate.state.testParams.focusGroupX = testimate.state.focusGroupDictionary[data.xAttData.name]; + /* + testimate.state.testParams.value + = testimate.state.valueDictionary[this.testID] + ? testimate.state.valueDictionary[this.testID] : 0.5; + */ + } + + } + + async updateTestResults() { + // todo: use exact binomial for small N, prop near 0 or 1 + const A = data.xAttData.theArray; + const G = testimate.state.testParams.focusGroupX; + + let N = 0; + this.results.successes = 0; + A.forEach( x => { + N++; + if (x === G) this.results.successes++; + }) + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + if (N > 0) { + const p0 = testimate.state.testParams.value; + const pHat = this.results.successes / N; // sample proportion p-hat + this.results.N = N; + this.results.prop = pHat; + + this.usingBinomial = (N * pHat < 10) || (N * (1 - pHat) < 10); // must have ≥ 10 successes AND 10 failures + + if (this.usingBinomial) { + + /** + * jStat.binomial.cdf(k, N, p) is the probability that you get between 0 and k successes in N trials + */ + if (pHat > p0) { // the sample prop is high, we'll find the upper tail + this.results.P = 1 - jStat.binomial.cdf(this.results.successes - 1, this.results.N, p0); // + } else { // the sample prop is LOW, we'll find the lower tail + this.results.P = jStat.binomial.cdf(this.results.successes, this.results.N, p0); // + } + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + if (this.results.P > 1) this.results.P = 1.00; + + this.results.SE = Math.sqrt((this.results.prop) * (1 - this.results.prop) / this.results.N); + this.results.z = ""; + this.results.zCrit = ""; + + const binomialResult = binomial.CIbeta(N, this.results.successes, testimate.state.testParams.alpha); + this.results.CImin = binomialResult[0]; + this.results.CImax = binomialResult[1]; + + } else { // not using binomial, using z + + this.results.SE = Math.sqrt((pHat) * (1 - pHat) / N); + const SEnull = Math.sqrt((p0) * (1 - p0) / N); + + // Note: test uses the SE of the null hypothesis (value); CI uses the SE of the sample. + this.results.z = (pHat - p0) / SEnull; // this.results.SE; + + this.results.zCrit = jStat.normal.inv(theCIparam, 0, 1); // 1.96-ish for 0.95 + const zAbs = Math.abs(this.results.z); + this.results.P = jStat.normal.cdf(-zAbs, 0, 1); + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + + // Note: CI uses the SE of the sample (this.results.SE) + this.results.CImax = pHat + this.results.zCrit * this.results.SE; + this.results.CImin = pHat - this.results.zCrit * this.results.SE; + } + } + } + + makeResultsString() { + + const N = this.results.N; + const successes = ui.numberToString(this.results.successes); + const prop = ui.numberToString(this.results.prop, 4); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const CImin = ui.numberToString(this.results.CImin); + const CImax = ui.numberToString(this.results.CImax); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + const value = ui.numberToString(testimate.state.testParams.value); + const sidesOp = testimate.state.testParams.theSidesOp; + + let out = "
";
+        const testQuestion = localize.getString("tests.oneSampleP.testQuestion",
+            data.xAttData.name, testimate.state.testParams.focusGroupX, sidesOp, value);
+        const r1 = localize.getString( "tests.oneSampleP.resultsLine1", prop, successes, N);
+
+        out += testQuestion;
+        out += `

${r1}`; + + if (this.usingBinomial) { + out += `
${P}`; + out += `
${conf}% ${localize.getString("CI")} = [${CImin}, ${CImax}]`; + out += `
(${localize.getString("tests.oneSampleP.usingBinomialProc")})`; + + } else { + const SE = ui.numberToString(this.results.SE); + const zCrit = ui.numberToString(this.results.zCrit, 3); + const z = ui.numberToString(this.results.z, 3); + + out += `
z = ${z}, ${P}`; + out += `
${conf}% ${localize.getString("CI")} = [${CImin}, ${CImax}]`; + out += `
SE = ${SE}, α = ${alpha}, z* = ${zCrit}`; + out += `
(${localize.getString("tests.oneSampleP.usingZProc")})`; + } + + out += `
`; + return out; + } + + makeTestDescription(iTestID, includeName) { + return `mean of ${testimate.state.x.name}`; + return + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + if(!testimate.state.focusGroupDictionary[data.xAttData.name]) { + testimate.setFocusGroup(data.xAttData, null); + } + const rememberedGroup = testimate.state.focusGroupDictionary[data.xAttData.name]; + + return localize.getString("tests.oneSampleP.menuString", + testimate.state.x.name, rememberedGroup); + } + + makeConfigureGuts() { + const configStart = localize.getString("tests.oneSampleP.configurationStart"); + + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value, 0.0, 1.0, 0.05); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + const group = ui.focusGroupButtonXHTML(testimate.state.testParams.focusGroupX); + let theHTML = `${configStart}(${data.xAttData.name} = ${group}) ${sides} ${value} ${conf}`; + + return theHTML; + } + + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/one-sample-t.js b/eepsmedia/plugins/testimate/src/tests/one-sample-t.js new file mode 100644 index 00000000..89652a2f --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/one-sample-t.js @@ -0,0 +1,90 @@ +class OneSampleT extends Test { + + constructor(iID) { + super(iID); + + testimate.state.testParams.value + = testimate.state.valueDictionary[this.testID] + ? testimate.state.valueDictionary[this.testID] : 0; + + } + + updateTestResults() { + const jX = jStat(data.xAttData.theArray); // jStat version of x array + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + this.results.N = jX.cols(); + this.results.df = this.results.N - 1; + this.results.mean = jX.mean(); + this.results.s = jX.stdev(true); // true means SAMPLE SD + this.results.SE = this.results.s / Math.sqrt(this.results.N); + this.results.P = jX.ttest(testimate.state.testParams.value, testimate.state.testParams.sides); + this.results.tCrit = jStat.studentt.inv(theCIparam, this.results.df); // 1.96-ish for 0.95 + this.results.CImax = this.results.mean + this.results.tCrit * this.results.SE; + this.results.CImin = this.results.mean - this.results.tCrit * this.results.SE; + this.results.t = (this.results.mean - testimate.state.testParams.value) / this.results.SE; + } + + makeResultsString() { + + const testDesc = `mean of ${testimate.state.x.name}`; + + const N = this.results.N; + const mean = ui.numberToString(this.results.mean, 3); + const s = ui.numberToString(this.results.s); + const SE = ui.numberToString(this.results.SE); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const CImin = ui.numberToString(this.results.CImin); + const CImax = ui.numberToString(this.results.CImax); + const tCrit = ui.numberToString(this.results.tCrit, 3); + const df = ui.numberToString(this.results.df, 3); + const t = ui.numberToString(this.results.t, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + const value = ui.numberToString(testimate.state.testParams.value); + + const testQuestion = localize.getString("tests.oneSampleT.testQuestion", + data.xAttData.name, testimate.state.testParams.theSidesOp, value); + const r2 = localize.getString("tests.oneSampleT.resultsLine2", mean, conf, CImin, CImax); + + let out = "
";
+
+        out += testQuestion;
+        out += `

N = ${N}, t = ${t}, ${P}`; + out += `
${r2}`; + out += `
s = ${s}, SE = ${SE}, df = ${df}, α = ${alpha}, t* = ${tCrit}`; + out += `
`; + + out += `
`; + return out; + } + + makeTestDescription( ) { + return `mean of ${testimate.state.x.name}`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.oneSampleT.menuString", testimate.state.x.name); + + // return `one-sample t mean of ${testimate.state.x.name}`; + } + + makeConfigureGuts() { + const configStart = localize.getString("tests.oneSampleT.configurationStart"); + + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${configStart}(${data.xAttData.name}) ${sides} ${value} ${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/paired.js b/eepsmedia/plugins/testimate/src/tests/paired.js new file mode 100644 index 00000000..69c098e5 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/paired.js @@ -0,0 +1,107 @@ +class Paired extends Test { + + + constructor(iID) { + super(iID); + testimate.state.testParams.reversed = false; + } + + updateTestResults() { + const X = data.xAttData.theArray; + const Y = data.yAttData.theArray; + const N = X.length; + if (N !== Y.length) { + alert(`Paired arrays are not the same length! Bogus results ahead!`); + } + let Z = []; + + for (let i = 0; i < N; i++) { + Z[i] = testimate.state.testParams.reversed ? Y[i] - X[i] : X[i] - Y[i]; + } + + const jX = jStat(Z); // jStat version of difference array + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + this.results.N = jX.cols(); + this.results.df = this.results.N - 1; + this.results.mean = jX.mean(); + this.results.s = jX.stdev(true); // true means SAMPLE SD + this.results.SE = this.results.s / Math.sqrt(this.results.N); + this.results.P = jX.ttest(testimate.state.testParams.value, testimate.state.testParams.sides); + this.results.tCrit = jStat.studentt.inv(theCIparam, this.results.df); // 1.96-ish for 0.95 + this.results.CImax = this.results.mean + this.results.tCrit * this.results.SE; + this.results.CImin = this.results.mean - this.results.tCrit * this.results.SE; + this.results.t = (this.results.mean - testimate.state.testParams.value) / this.results.SE; + } + + makeResultsString() { + const N = this.results.N; + const mean = ui.numberToString(this.results.mean, 3); + const s = ui.numberToString(this.results.s); + const SE = ui.numberToString(this.results.SE); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const CImin = ui.numberToString(this.results.CImin); + const CImax = ui.numberToString(this.results.CImax); + const tCrit = ui.numberToString(this.results.tCrit, 3); + const df = ui.numberToString(this.results.df, 3); + const t = ui.numberToString(this.results.t, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + const value = ui.numberToString(testimate.state.testParams.value); + + const testQuestion = testimate.state.testParams.reversed ? + localize.getString("tests.paired.testQuestion", + testimate.state.y.name, testimate.state.x.name, testimate.state.testParams.theSidesOp, value) : + localize.getString("tests.paired.testQuestion", + testimate.state.x.name, testimate.state.y.name, testimate.state.testParams.theSidesOp, value) ; + const r2 = localize.getString( "tests.paired.resultsLine2", mean, conf, CImin, CImax); + + let out = "
";
+
+        out += testQuestion;
+        out += `

N = ${N}, t = ${t}, ${P}`; + out += `
${r2}`; + out += `
s = ${s}, SE = ${SE}, df = ${df}, α = ${alpha}, t* = ${tCrit} `; + out += `
`; + + out += `
`; + return out; + } + + makeTestDescription( ) { + return `paired test of ${data.xAttData.name} - ${data.yAttData.name}`; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + // return `paired test of ${data.xAttData.name} - ${data.yAttData.name}`; + if (testimate.state.testParams.reversed) { + return localize.getString("tests.paired.menuString", testimate.state.y.name, testimate.state.x.name); + } else { + return localize.getString("tests.paired.menuString", testimate.state.x.name, testimate.state.y.name); + } + } + + makeConfigureGuts() { + const configStart = localize.getString("tests.paired.configurationStart"); + + const chicletGuts = (testimate.state.testParams.reversed) ? + `${testimate.state.y.name} – ${testimate.state.x.name}` : + `${testimate.state.x.name} – ${testimate.state.y.name}` ; + + const chiclet = ui.chicletButtonHTML(chicletGuts); + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${configStart}
 ${chiclet} ${sides} ${value}

${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/regression.js b/eepsmedia/plugins/testimate/src/tests/regression.js new file mode 100644 index 00000000..c5682e8d --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/regression.js @@ -0,0 +1,142 @@ +class Regression extends Test { + + constructor(iID, iGrouping) { + super(iID); + } + + updateTestResults() { + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + let sumXY = 0; + let sumX = 0; + let sumXX = 0; + let sumYY = 0; + let sumY = 0; + let N = data.xAttData.theArray.length; + + if (N > 2) { + for (let i = 0; i < N; i++) { + // Note how these definitions are REVERSED. + // we want to look at the var in the first position (xAttData) as the dependent variable (Y) + const X = data.yAttData.theArray[i]; + const Y = data.xAttData.theArray[i]; + sumX += X; + sumY += Y; + sumXY += X * Y; + sumXX += X * X; + sumYY += Y * Y; + } + + const slope = (N * sumXY - sumX * sumY) / (N * sumXX - sumX ** 2); + const intercept = (sumY - slope * sumX) / N; + const SDsqError = 1 / (N * (N - 2)) * (N * sumYY - sumY ** 2 - slope ** 2 * (N * sumXX - sumX ** 2)); + const SDsqSlope = N * SDsqError / (N * sumXX - sumX ** 2); + const SDsqIntercept = SDsqSlope / N * sumXX; + const r = (N * sumXY - sumX * sumY) / + Math.sqrt((N * sumXX - sumX ** 2) * (N * sumYY - sumY ** 2)); + const rsq = r * r; + + + this.results.N = N; + this.results.slope = slope; + this.results.intercept = intercept; + this.results.df = N - 2; + this.results.tCrit = jStat.studentt.inv(theCIparam, this.results.df); // 1.96-ish for 0.95 + this.results.SEslope = SDsqSlope; + this.results.SEintercept = SDsqIntercept; + this.results.rho = r; + this.results.rsq = rsq; + + const SDslope = Math.sqrt(SDsqSlope); + const SDintercept = Math.sqrt(SDsqIntercept); + + this.results.slopeCImin = slope - this.results.tCrit * SDslope; + this.results.slopeCImax = slope + this.results.tCrit * SDslope; + this.results.interceptCImin = intercept - this.results.tCrit * SDintercept; + this.results.interceptCImax = intercept + this.results.tCrit * SDintercept; + + // test slope against value + this.results.t = (this.results.slope - testimate.state.testParams.value) / SDslope; + const tAbs = Math.abs(this.results.t); + this.results.P = jStat.studentt.cdf(-tAbs, this.results.df); + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + } + + } + + makeResultsString() { + // const testDesc = `mean of ${testimate.state.x.name}`; + const N = this.results.N; + + const slope = ui.numberToString(this.results.slope); // CI of slope + const intercept = ui.numberToString(this.results.intercept); // CI of slope + const CISmin = ui.numberToString(this.results.slopeCImin); // CI of slope + const CISmax = ui.numberToString(this.results.slopeCImax); + const CIImin = ui.numberToString(this.results.interceptCImin); // CI of intercept + const CIImax = ui.numberToString(this.results.interceptCImax); + const df = ui.numberToString(this.results.df); + const rho = ui.numberToString(this.results.rho); + const rsq = ui.numberToString(this.results.rsq); + const t = ui.numberToString(this.results.t, 3); + const tCrit = ui.numberToString(this.results.tCrit, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + + const theSign = intercept >= 0 ? "+" : '-'; + + const X = testimate.state.x.name; + const Y = testimate.state.y.name; + + const DSdetails = document.getElementById("DSdetails"); + const DSopen = DSdetails && DSdetails.hasAttribute("open"); + + const testingSlopePhrase = localize.getString("tests.regression.testingSlope"); + const slopeWord = localize.getString("slope"); + const interceptWord = localize.getString("intercept"); + + let out = "
";
+
+        //  out += `How does (${X}) depend on (${Y})?`
+        out += localize.getString("tests.regression.testQuestion", X, Y);
+        out += `
LSRL: ${X} = ${slope} (${Y}) ${theSign} ${Math.abs(intercept)} `; // note reversal! + out += `
N = ${N}, ρ = ${rho}, r2 = ${rsq}
`; + out += `
`; + out += localize.getString("tests.regression.detailsSummary", X, Y); + out += ``; + out += `
${slopeWord}${slope}${conf}% ${localize.getString("CI")} = [${CISmin}, ${CISmax}]
${interceptWord}${intercept}${conf}% ${localize.getString("CI")} = [${CIImin}, ${CIImax}]
`; + out += `
`; + out += `${testingSlopePhrase} ${testimate.state.testParams.theSidesOp} ${testimate.state.testParams.value} ` + out += `
t = ${t}, ${P}`; + out += `
df = ${df}, α = ${alpha}, t* = ${tCrit}, ` + out += `
`; + out += `
`; + out += `
`; + + return out; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString() { + return localize.getString("tests.regression.menuString",testimate.state.x.name, testimate.state.y.name); + // return `linear regression of (${testimate.state.x.name}) as a function of (${testimate.state.y.name})`; + } + + makeConfigureGuts() { + const testingSlopePhrase = localize.getString("tests.regression.testingSlope"); + + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${testingSlopePhrase} ${sides} ${value} ${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/two-sample-p.js b/eepsmedia/plugins/testimate/src/tests/two-sample-p.js new file mode 100644 index 00000000..64fa26f1 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/two-sample-p.js @@ -0,0 +1,230 @@ +class TwoSampleP extends Test { + + constructor(iID, iGrouping) { + super(iID); + this.grouping = iGrouping; + this.results.successValueA = null; // label for principal value for group A + this.results.successValueB = null; // label for principal value for B + + // get a default "group" -- the value we count as "success" for proportions + if (!testimate.restoringFromSave || !testimate.state.testParams.focusGroupX) { + testimate.state.testParams.focusGroupX = testimate.state.focusGroupDictionary[data.xAttData.name]; + testimate.state.testParams.focusGroupY = testimate.state.focusGroupDictionary[data.yAttData.name]; + } + testimate.state.testParams.value + = testimate.state.valueDictionary[this.testID] + ? testimate.state.valueDictionary[this.testID] : 0; + + } + + + updateTestResults() { + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + + let A = data.xAttData.theArray; + let B = data.yAttData.theArray; + + if (this.grouping) { + // A (X) holds the data and values + // B (Y) holds the group membership. + + this.results.labelA = testimate.state.testParams.focusGroupY; // theGroups[0]; + this.results.labelB = Test.getComplementaryValue( data.yAttData, this.results.labelA); + + this.results.successValueA = testimate.state.testParams.focusGroupX; + // this.results.successValueA || theValues[0]; // the default principal group = the first, by default + this.results.successValueB = testimate.state.testParams.focusGroupX; // must be the same as for A if we're grouped + + [A, B] = Test.splitByGroup(A, B, this.results.labelA); + + } else { + this.results.labelA = data.xAttData.name; + this.results.labelB = data.yAttData.name; + + // const theAValues = [...data.xAttData.valueSet]; + this.results.successValueA = testimate.state.testParams.focusGroupX; // this.results.successValueA || theAValues[0]; // the default principal group = the first, by default + const theBValues = [...data.yAttData.valueSet]; + if (theBValues.includes(this.results.successValueA)) { + // we don't do the "or" here so that if the value exists in A, + // a change will "drag" B along. + // There is a chance this is not what the user wants. + this.results.successValueB = this.results.successValueA; + } else { + this.results.successValueB = testimate.state.testParams.focusGroupY; + } + } + + // count cases and successes in "A" + this.results.N1 = 0; + this.results.successesA = 0; + A.forEach( a => { + this.results.N1++; + if (a === this.results.successValueA) this.results.successesA++ + }) + + // count cases and successes in "B" + this.results.N2 = 0; + this.results.successesB = 0; + B.forEach( b => { + this.results.N2++; + if (b === this.results.successValueB) this.results.successesB++ + }) + + this.results.N = this.results.N1 + this.results.N2; + if (this.results.N1 > 0 && this.results.N2 > 0) { + const pHat = (this.results.successesA + this.results.successesB) / this.results.N; // p (pooled) + const qHat = 1 - pHat; + this.results.prop = pHat; + + this.results.prop1 = this.results.successesA / this.results.N1; + this.results.prop2 = this.results.successesB / this.results.N2; + this.results.SE1 = Math.sqrt(this.results.prop1 * (1 - this.results.prop1) / this.results.N1); + this.results.SE2 = Math.sqrt(this.results.prop2 * (1 - this.results.prop2) / this.results.N2); + + // pooled standard error + this.results.SE = Math.sqrt((pHat * qHat) * (1/ this.results.N1 + 1 / this.results.N2)); + + this.results.SEinterval = Math.sqrt( + this.results.prop1 * (1 - this.results.prop1) / this.results.N1 + + this.results.prop2 * (1 - this.results.prop2) / this.results.N2 + ) + + // the test p1 - p2 + this.results.pDiff = this.results.prop1 - this.results.prop2; + + // test statistic = z + this.results.z = (this.results.pDiff - testimate.state.testParams.value) / this.results.SE; + this.results.zCrit = jStat.normal.inv(theCIparam, 0, 1); // 1.96-ish for 0.95 + + const zAbs = Math.abs(this.results.z); + this.results.P = jStat.normal.cdf(-zAbs, 0, 1); + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + + this.results.CImax = this.results.pDiff + this.results.zCrit * this.results.SEinterval; + this.results.CImin = this.results.pDiff - this.results.zCrit * this.results.SEinterval; + } + } + + makeResultsString() { + const N = this.results.N; + const N2 = this.results.N2; + const N1 = this.results.N1; + const pDiff = ui.numberToString(this.results.pDiff, 3); + const SE = ui.numberToString(this.results.SE); + const SEinterval = ui.numberToString(this.results.SEinterval); + + const p1 = ui.numberToString(this.results.prop1); + const p2 = ui.numberToString(this.results.prop2); + + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const CImin = ui.numberToString(this.results.CImin); + const CImax = ui.numberToString(this.results.CImax); + const zCrit = ui.numberToString(this.results.zCrit, 3); + + const z = ui.numberToString(this.results.z, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + + const DSdetails = document.getElementById("DSdetails"); + const DSopen = DSdetails && DSdetails.hasAttribute("open"); + let out = "
";
+
+        const groupingPhrase = `(${testimate.state.x.name} = ${this.results.successValueA}): ${this.results.labelA} - ${this.results.labelB}`;
+        const nonGroupingPhrase = `(${testimate.state.x.name} = ${this.results.successValueA}) - (${testimate.state.y.name} = ${this.results.successValueB})`;
+
+        const comparison = `${testimate.state.testParams.theSidesOp} ${testimate.state.testParams.value}`;
+        const resultHed = (this.grouping) ?
+            `${localize.getString("tests.twoSampleP.testQuestionHead")} ${groupingPhrase} ${comparison}?` :
+            `${localize.getString("tests.twoSampleP.testQuestionHead")} ${nonGroupingPhrase} ${comparison}?`;
+
+        out += `${resultHed} 
`; + out += `
N = ${N}, diff = ${pDiff}, z = ${z}, ${P}`; + out += `
${conf}% CI = [${CImin}, ${CImax}], SE(CI) = ${SEinterval} `; + + out += `
`; + out += localize.getString("tests.twoSampleP.detailsSummary"); + out += this.makeTwoSampleTable(); + out += `
α = ${alpha}, z* = ${zCrit}

` + out += `
`; + + out += `
`; + + return out; + } + + makeTwoSampleTable() { + const SE1 = ui.numberToString(this.results.SE1); + const SE2 = ui.numberToString(this.results.SE2); + const SE = ui.numberToString(this.results.SE); + const N2 = this.results.N2; + const N1 = this.results.N1; + const N = this.results.N; + const succA = this.results.successesA; + const succB = this.results.successesB; + const p1 = ui.numberToString(this.results.prop1); + const p2 = ui.numberToString(this.results.prop2); + const prop = ui.numberToString(this.results.prop); + + const groupColHead = this.grouping ? `${data.yAttData.name}` : localize.getString("group"); + const propColHead = this.grouping ? + `${localize.getString("proportion")}
${data.xAttData.name} = ${this.results.successValueA}` : + `${localize.getString("proportion")}`; + const pooled = localize.getString("pooled"); + + let out = ""; + + const groupRowLabelA = this.grouping ? this.results.labelA : `${this.results.labelA} = ${this.results.successValueA}`; + const groupRowLabelB = this.grouping ? this.results.labelB : `${this.results.labelB} = ${this.results.successValueB}`; + + out += ``; + out += ``; + out += ``; + out += ``; + out += ``; + out += `
${groupColHead}N${propColHead}SE
${groupRowLabelA}${succA} / ${N1}${p1}${SE1}
${groupRowLabelB}${succB} / ${N2}${p2}${SE2}
${pooled}${succA + succB} / ${N}${prop}${SE}
`; + + return out + } + + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString(iID) { + if (iID === `BB02`) { + return localize.getString("tests.twoSampleP.menuString1", testimate.state.x.name, testimate.state.y.name); + } else { + return localize.getString("tests.twoSampleP.menuString2", testimate.state.x.name, testimate.state.y.name); + } + } + + makeConfigureGuts() { + const configStart = localize.getString("tests.twoSampleP.configStart"); + + const intro = (this.grouping) ? + `${configStart}:
 (${testimate.state.x.name} = ${ui.focusGroupButtonXHTML(testimate.state.testParams.focusGroupX)} ) : ${ui.focusGroupButtonYHTML(testimate.state.testParams.focusGroupY)} - ${this.results.labelB}` : + `${configStart}:
 (${testimate.state.x.name} = ${ui.focusGroupButtonXHTML(testimate.state.testParams.focusGroupX)}) - (${testimate.state.y.name} = ${ui.focusGroupButtonYHTML(testimate.state.testParams.focusGroupY)}) `; + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value, 0.0, 1.0, .05); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + let theHTML = `${intro} ${sides} ${value}
 ${conf}`; + + return theHTML; + } + +/* + successValueButtonA( ) { + return `` + } + + successValueButtonB( ) { + return `` + } +*/ + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/tests/two-sample-t.js b/eepsmedia/plugins/testimate/src/tests/two-sample-t.js new file mode 100644 index 00000000..9e93e143 --- /dev/null +++ b/eepsmedia/plugins/testimate/src/tests/two-sample-t.js @@ -0,0 +1,216 @@ +/** + * Implements two forms of a two-sample t test. + * + * **Two separate attributes**: We compare the mean value in "X" to the mean value in "Y." + * This is perfect if you have weights of cats in one column and dogs in another, + * *and they are not paired*. (There could be different numbers of animals...) + * + * **Y is a grouping attribute**: We split the values of "X" according to values in "Y." + * Use this if you have weights of all animals in the "X" column and the values `cat` or `dog` + * in "Y". (i.e., tidy) + * + * The member `this.grouping` tells which kind of test it is. + */ +class TwoSampleT extends Test { + + constructor(iID, iGrouping) { + super(iID); + this.grouping = iGrouping; // is a grouping value in "Y"? + this.results.groupNames = []; // names of the two groups to be displayed (depends on grouping) + if (this.grouping) { + if (!testimate.restoringFromSave || !testimate.state.testParams.focusGroupY) { + testimate.state.testParams.focusGroupY = testimate.state.focusGroupDictionary[data.yAttData.name]; + } + + } else { + testimate.state.testParams.focusGroupY = null; + } + testimate.state.testParams.value + = testimate.state.valueDictionary[this.testID] + ? testimate.state.valueDictionary[this.testID] : 0; + + testimate.state.testParams.reversed = false; + } + + updateTestResults() { + + const theCIparam = 1 - testimate.state.testParams.alpha / 2; + let A = data.xAttData.theArray; + let B = data.yAttData.theArray; + this.results.group1Name = data.xAttData.name; + this.results.group2Name = data.yAttData.name; + + if (this.grouping) { + [A, B] = Test.splitByGroup(A, B, testimate.state.testParams.focusGroupY); + console.log(`A = ${A}, B = ${B}`); + this.results.group1Name = testimate.state.testParams.focusGroupY; // the name of a value in the second att + this.results.group2Name = data.yAttData.isBinary() ? + handlers.nextValueInList([...data.yAttData.valueSet], testimate.state.testParams.focusGroupY) : // the OTHER value + `not ${testimate.state.testParams.focusGroupY}` // or a more general label, NOT "a" + } + + const j0 = jStat(A); + const j1 = jStat(B); + + this.results.N1 = j0.cols(); + this.results.N2 = j1.cols(); + this.results.N = this.results.N1 + this.results.N2; + + this.results.df = this.results.N1 + this.results.N2 - 2; + this.results.mean1 = j0.mean(); + this.results.mean2 = j1.mean(); + this.results.s1 = j0.stdev(true); // true means SAMPLE SD + this.results.s2 = j1.stdev(true); // true means SAMPLE SD + this.results.SE1 = this.results.s1 / Math.sqrt(this.results.N1); + this.results.SE2 = this.results.s2 / Math.sqrt(this.results.N2); + + /* + See https://en.wikipedia.org/wiki/Student%27s_t-test#Independent_two-sample_t-test. + I'm using "Equal or unequal sample sizes, similar variance." + Maybe we should go one further and use Welch's, which follows + in that wikipedia article. + */ + const sArg = ((this.results.N1 - 1) * this.results.s1 ** 2 + + (this.results.N2 - 1) * this.results.s2 ** 2) / + (this.results.N1 + this.results.N2 - 2); + this.results.s = Math.sqrt(sArg); // pooled SD + this.results.SE = this.results.s * Math.sqrt((1 / this.results.N1) + (1 / this.results.N2)); + this.results.diff = testimate.state.testParams.reversed ? + this.results.mean2 - this.results.mean1 : this.results.mean1 - this.results.mean2; + this.results.t = (this.results.diff - testimate.state.testParams.value) / this.results.SE; + + const var1oN = j0.variance(true) / this.results.N1; + const var2oN = j1.variance(true) / this.results.N2; // sample variance/N = s^2/N + // const df2 = (var1oN + var2oN) ** 2 / (var1oN ** 2 / (this.results.N1 - 1) + var2oN ** 2 / (this.results.N2)); // variance for + // const df1 = this.results.N1 + this.results.N2 - 1; + + // this.results.df = df2; // just use the df calculated earlier: N1 + N2 - 2. + + this.results.tCrit = jStat.studentt.inv(theCIparam, this.results.df); // 1.96-ish for 0.95 + const tAbs = Math.abs(this.results.t); + this.results.P = jStat.studentt.cdf(-tAbs, this.results.df); + if (testimate.state.testParams.sides === 2) this.results.P *= 2; + + this.results.CImax = this.results.diff + this.results.tCrit * this.results.SE; + this.results.CImin = this.results.diff - this.results.tCrit * this.results.SE; + + } + + makeResultsString() { + + const N = this.results.N; + const diff = ui.numberToString(this.results.diff, 3); + const s = ui.numberToString(this.results.s); + const SE = ui.numberToString(this.results.SE); + + const mean1 = ui.numberToString(this.results.mean1); + const mean2 = ui.numberToString(this.results.mean2); + const P = (this.results.P < 0.0001) ? + `P < 0.0001` : + `P = ${ui.numberToString(this.results.P)}`; + const CImin = ui.numberToString(this.results.CImin); + const CImax = ui.numberToString(this.results.CImax); + const tCrit = ui.numberToString(this.results.tCrit, 3); + const df = ui.numberToString(this.results.df, 3); + const t = ui.numberToString(this.results.t, 3); + const conf = ui.numberToString(testimate.state.testParams.conf); + const alpha = ui.numberToString(testimate.state.testParams.alpha); + + const DSdetails = document.getElementById("DSdetails"); + const DSopen = DSdetails && DSdetails.hasAttribute("open"); + + const comparison = `${testimate.state.testParams.theSidesOp} ${testimate.state.testParams.value}`; + + const resultHed = (this.grouping) ? + localize.getString("tests.twoSampleT.testQuestion1", testimate.state.x.name,this.results.group1Name,this.results.group2Name,comparison) : + testimate.state.testParams.reversed ? + localize.getString("tests.twoSampleT.testQuestion2", testimate.state.y.name,testimate.state.x.name,comparison) : + localize.getString("tests.twoSampleT.testQuestion2", testimate.state.x.name,testimate.state.y.name,comparison) ; + + let out = "
";
+
+        out += `${resultHed} 
`; + out += `
N = ${N}, t = ${t}, ${P}`; + out += `
diff = ${diff}, ${conf}% ${localize.getString("CI")} = [${CImin}, ${CImax}] `; + + out += `
`; + out += localize.getString("tests.twoSampleT.detailsSummary"); // `Difference of means, t procedure`; + out += this.makeTwoSampleTable(); + out += `
df = ${df}, α = ${alpha}, t* = ${tCrit}` + out += `
`; + + out += `
`; + + return out; + } + + makeTwoSampleTable() { + const N2 = this.results.N2; + const N1 = this.results.N1; + const s1 = ui.numberToString(this.results.s1); + const s2 = ui.numberToString(this.results.s2); + const SE1 = ui.numberToString(this.results.SE1); + const SE2 = ui.numberToString(this.results.SE2); + const mean1 = ui.numberToString(this.results.mean1); + const mean2 = ui.numberToString(this.results.mean2); + + const N = this.results.N; + const diff = ui.numberToString(this.results.diff, 3); + const s = ui.numberToString(this.results.s); + const SE = ui.numberToString(this.results.SE); + + const mean = localize.getString("mean"); + const group = localize.getString("group"); + + const groupColHed = this.grouping ? `${testimate.state.y.name}` : group; + const meanColHead = this.grouping ? `${mean}(${testimate.state.x.name})` : mean; + + let out = ""; + out += ``; + out += ``; + out += ``; + out += ``; + out += `
${groupColHed}N${meanColHead}sSE
${this.results.group1Name}${N1}${mean1}${s1}${SE1}
${this.results.group2Name}${N2}${mean2}${s2}${SE2}
pooled${N}diff =
${diff}
${s}${SE}
`; + return out; + } + + /** + * NB: This is a _static_ method, so you can't use `this`! + * @returns {string} what shows up in a menu. + */ + static makeMenuString(iID) { + if (iID === `NN02`) { + return localize.getString("tests.twoSampleT.menuString1", testimate.state.x.name, testimate.state.y.name); + } else { + return localize.getString("tests.twoSampleT.menuString2", testimate.state.x.name, testimate.state.y.name); + } + } + + makeConfigureGuts() { + + const yComplement = Test.getComplementaryValue(data.yAttData, testimate.state.testParams.focusGroupY); + const configStart = (this.grouping) ? + localize.getString("tests.twoSampleT.configStartPaired", testimate.state.x.name) : + localize.getString("tests.twoSampleT.configStartUnpaired"); + + const chicletGuts = (testimate.state.testParams.reversed) ? + `mean(${testimate.state.y.name}) – mean(${testimate.state.x.name})` : + `mean(${testimate.state.x.name}) – mean(${testimate.state.y.name})` ; + + const chiclet = ui.chicletButtonHTML(chicletGuts); + + const configContinues = (this.grouping) ? + `[${ui.focusGroupButtonYHTML(testimate.state.testParams.focusGroupY)}]–[${yComplement}]` : + chiclet ; + + + const sides = ui.sidesBoxHTML(testimate.state.testParams.sides); + const value = ui.valueBoxHTML(testimate.state.testParams.value); + const conf = ui.confBoxHTML(testimate.state.testParams.conf); + + let theHTML = `${configStart}:
 ${configContinues} ${sides} ${value}
 ${conf}`; + + return theHTML; + } + +} \ No newline at end of file diff --git a/eepsmedia/plugins/testimate/src/ui.js b/eepsmedia/plugins/testimate/src/ui.js new file mode 100644 index 00000000..6873bdff --- /dev/null +++ b/eepsmedia/plugins/testimate/src/ui.js @@ -0,0 +1,400 @@ +let ui; + +ui = { + + xDIV: null, + xNameDIV: null, + yDIV: null, + yNameDIV: null, + xType: null, + yType: null, + + datasetDIV: null, + datasetSPAN: null, + testHeaderDIV: null, + resultsDIV: null, // results DIV + configDIV: null, + emitControls: null, + + emitMode: "single", + + initialize: function () { + this.xDIV = document.getElementById(`xDIV`); + this.yDIV = document.getElementById(`yDIV`); + this.xType = document.getElementById(`xCNbutton`); + this.yType = document.getElementById(`yCNbutton`); + + this.xNameDIV = document.getElementById(`xAttributeName`); + this.yNameDIV = document.getElementById(`yAttributeName`); + + this.datasetDIV = document.getElementById(`datasetDIV`); + this.datasetSPAN = document.getElementById(`datasetSPAN`); + this.testHeaderDIV = document.getElementById(`testHeaderDIV`); + this.resultsDIV = document.getElementById(`resultsDIV`); + this.configDIV = document.getElementById(`configureDIV`); + + this.emitControls = document.getElementById(`emitControls`); + this.emitMode = "single"; + }, + + /** + * Main UI function. Redraws the screen. + * + * @returns {Promise} + */ + redraw: async function () { + + if (testimate.state.dataset) { + + if (testimate.theTest && testimate.theTest.testID) { + + // create the text and other display information for the results + this.datasetSPAN.innerHTML = await this.makeDatasetGuts(); + this.testHeaderDIV.innerHTML = this.makeTestHeaderGuts(); // includes making the choice menu + this.resultsDIV.innerHTML = testimate.theTest.makeResultsString(); + this.configDIV.innerHTML = testimate.theTest.makeConfigureGuts(); + document.getElementById("randomEmitNumberBox").value = testimate.state.randomEmitNumber; + this.adjustEmitGuts(); + } + } + + this.updateAttributeBlocks(); + this.setVisibility(); + }, + + setVisibility: function () { + + // many things are invisible if there is no x-variable, therefore no test + + document.getElementById('Ybackdrop').style.display = (testimate.state.x) ? 'inline' : 'none'; + // document.getElementById('xCNbutton').style.display = (testimate.state.x) ? 'inline' : 'none'; + document.getElementById('testHeaderDIV').style.display = (testimate.state.x) ? 'block' : 'none'; + document.getElementById('emitDIV').style.display = (testimate.state.x) ? 'block' : 'none'; + document.getElementById('resultsDIV').style.display = (testimate.state.x) ? 'block' : 'none'; + document.getElementById('configureDIV').style.display = (testimate.state.x) ? 'block' : 'none'; + + document.getElementById('emitSingleGroup').style.display = "block"; // always show single + document.getElementById('emitRandomGroup').style.display = (data.hasRandom) ? "block" : "none"; + document.getElementById('emitHierarchicalGroup').style.display = (data.isGrouped) ? "block" : "none"; + + // emit mode visibility + + switch (this.emitMode) { + case "single": + document.getElementById('emitSingleButton').style.display = 'inline'; + document.getElementById('chooseEmitSingle').checked = true; + document.getElementById('emitRandomButton').style.display = 'none'; + // document.getElementById('chooseEmitRandomLabel').style.display = 'inline'; + document.getElementById('randomEmitNumberBox').style.display = 'none'; + document.getElementById('randomEmitNumberBoxLabel').style.display = 'none'; + document.getElementById('emitHierarchyButton').style.display = 'none'; + break; + case "random": + document.getElementById('chooseEmitRandom').checked = true; + document.getElementById('emitSingleButton').style.display = 'none'; + document.getElementById('emitRandomButton').style.display = 'inline'; + document.getElementById('chooseEmitRandomLabel').style.display = 'inline'; + document.getElementById('randomEmitNumberBox').style.display = 'inline'; + document.getElementById('randomEmitNumberBoxLabel').style.display = 'inline'; + document.getElementById('emitHierarchyButton').style.display = 'none'; + break; + case "hierarchy": + document.getElementById('chooseEmitHierarchy').checked = true; + document.getElementById('emitSingleButton').style.display = 'none'; + document.getElementById('emitRandomButton').style.display = 'none'; + // document.getElementById('chooseEmitRandomLabel').style.display = 'inline'; + document.getElementById('randomEmitNumberBox').style.display = 'none'; + document.getElementById('randomEmitNumberBoxLabel').style.display = 'none'; + document.getElementById('emitHierarchyButton').style.display = 'inline'; + break; + default: + alert(`unexpected emit mode: [${this.emitMode}]`); + break; + } + + + }, + + updateAttributeBlocks: function () { + const xType = document.getElementById(`xCNbutton`); + const yType = document.getElementById(`yCNbutton`); + const xTrash = document.getElementById(`xTrashAttButton`); + const yTrash = document.getElementById(`yTrashAttButton`); + + if (testimate.state.x && testimate.state.x.name) { + this.xNameDIV.textContent = testimate.state.x.name; + xType.value = testimate.state.dataTypes[testimate.state.x.name] === 'numeric' ? '123' : 'abc'; + xTrash.style.display = "inline"; + xType.style.display = "inline"; + this.xDIV.className = "drag-none"; + } else { // x attribute waiting for drop! + this.xNameDIV.textContent = localize.getString("dropAttributeHere"); + xTrash.style.display = "none"; + xType.style.display = "none"; + this.xDIV.className = "drag-empty"; + } + if (testimate.state.y && testimate.state.y.name) { + this.yNameDIV.textContent = testimate.state.y.name; + yType.value = testimate.state.dataTypes[testimate.state.y.name] === 'numeric' ? '123' : 'abc'; + yTrash.style.display = "inline"; + yType.style.display = "inline"; + this.yDIV.className = "drag-none"; + } else { + this.yNameDIV.textContent = localize.getString("dropAttributeHere"); + yTrash.style.display = "none"; + yType.style.display = "none"; + this.yDIV.className = "drag-empty"; + } + + }, + + numberToString: function (iValue, iFigs = 4) { + let out = ""; + let multiplier = 1; + let suffix = ""; + let exponential = false; + + if (iValue === "" || iValue === null || typeof iValue === "undefined") { + out = ""; + } else if (iValue === 0) { + out = "0"; + } else { + if (Math.abs(iValue) > 1.0e15) { + exponential = true; + } else if (Math.abs(iValue) < 1.0e-4) { + exponential = true; + } else if (Math.abs(iValue) > 1.0e10) { + multiplier = 1.0e9; + iValue /= multiplier; + suffix = " B"; + } else if (Math.abs(iValue) > 1.0e7) { + multiplier = 1.0e6; + iValue /= multiplier; + suffix = " M"; + } + out = new Intl.NumberFormat( + testimate.constants.lang, + {maximumSignificantDigits: iFigs, useGrouping: false} + ).format(iValue); + + if (exponential) { + out = Number.parseFloat(iValue).toExponential(iFigs); + } + } + return `${out}${suffix}`; // empty if null or empty + }, + + /** + * returns the "sides" button HTML, which controls whether this is a 1- or 2-sided test. + * The button therefore changes from "≠" to either ">" or "<", and back again. + * This is in the form of a clickable button so you can change it. + * + * @param iSides + * @returns string containing the html for that button + */ + sidesBoxHTML: function (iSides) { + const theParams = testimate.state.testParams; + theParams.theSidesOp = "≠"; + if (iSides === 1) { + const testStat = testimate.theTest.results[testimate.theTest.theConfig.testing]; // testing what? mean? xbar? diff? slope? + theParams.theSidesOp = (testStat > theParams.value ? ">" : "<"); + } + + return `` + }, + + /** + * Button that changes which group is compared to everybody else + * (we will call this group the "focusGroup" + * (when a categorical app needs to be made binary) + * @param iGroup + * @returns {``} + */ + focusGroupButtonXHTML: function (iGroup) { + return `` + }, + + focusGroupButtonYHTML: function (iGroup) { + return `` + }, + + chicletButtonHTML : function(iGuts) { + return `` + }, + + sides12ButtonHTML : function(iSides) { + const buttonTitle = localize.getString("Nsided", iSides); + return `` + }, + + getFocusGroupName: function () { + if (!testimate.state.testParams.focusGroup) { + testimate.setFocusGroup(data.xAttData, null); + } + return testimate.state.focusGroupDictionary[data.xAttData.name]; + }, + + makeLogisticGraphButtonHTML: function (iGroup) { + const theLabel = localize.getString("showGraph"); + return `` + }, + + /** + * Construct a number to receive a value such as + * the value to be compared to + * + * @param iVal + * @param iMax + * @param iStep + * @returns {``} + */ + valueBoxHTML: function (iVal, iMin, iMax, iStep) { + const minPhrase = iMin ? `min="${iMin}"` : ""; + const maxPhrase = iMax ? `max="${iMax}"` : ""; + const stepPhrase = iStep ? `step="${iStep}"` : ""; + return ``; + }, + + iterBoxHTML: function (iVal, iMax, iStep) { + const maxPhrase = iMax ? `max="${iMax}"` : ""; + const stepPhrase = iStep ? `step="${iStep}"` : ""; + return ``; + }, + + rateBoxHTML: function (iVal, iMax, iStep) { + const maxPhrase = iMax ? `max="${iMax}"` : ""; + const stepPhrase = iStep ? `step="${iStep}"` : ""; + return ``; + }, + + logisticRegressionProbeBoxHTML: function (iVal, iMax, iStep) { + const maxPhrase = iMax ? `max="${iMax}"` : ""; + const stepPhrase = iStep ? `step="${iStep}"` : ""; + return ``; + }, + + /** + * Construct a number to receive a + * a confidence level. Also includes a